# JiraMetrics - full documentation The JiraMetrics documentation pages, concatenated for LLM ingestion. See https://jirametrics.org/llms.txt for a shorter index with per-page links. ================================================================================ # Installation / Upgrade instructions Source: https://jirametrics.org/install/ ================================================================================ Version 3.0 now requires a newer version of ruby. See the [changelog](https://jirametrics.org/changes/) for details. JiraMetrics should run on any Ruby runtime that supports version **3.4** or higher. Instructions below are for CRuby, which is also known as MRI, and for JRuby, which runs on the Java Virtual Machine (JVM). Unless you have a reason to want to run on the JVM, I'd recommend using CRuby. ## CRuby (formerly called MRI Ruby) ### Install If you're on a mac, you may have noticed that Ruby is already installed on your machine. The bad news is that it's likely too old to be useful. Run `ruby -v` and if the version is less than 3.4 then you'll still need to install a newer version. Homebrew installs Ruby but doesn't always put it ahead of the macOS system Ruby on your `PATH`. If `ruby -v` still shows the old version in a new terminal after installing, add Homebrew's Ruby to your `PATH` (for example add `export PATH="$(brew --prefix ruby)/bin:$PATH"` to your shell profile), then open a new terminal. Otherwise `gem install` may install into the wrong Ruby and the `jirametrics` command won't be found. 1. Install CRuby itself with [the instructions here](https://www.ruby-lang.org/en/downloads/). If you're using Windows, there is a one-click installer. If you're on almost any other platform, your local package manager will have a way to do that. In my case, I'm using a mac so it's as simple as `brew install ruby` 2. Once Ruby is installed, you can install JiraMetrics with this command. ``` gem install jirametrics ``` 3. You can then invoke `jirametrics` directly from the terminal. Confirm it installed and is on your `PATH` by running `jirametrics --version`, which should print the version number. If you instead get "command not found", see the `PATH` tip above. 4. Optional: If you want to use the dependency report then you'll want to install [graphviz](https://graphviz.org/download/) ### Upgrade When it's already installed, you can upgrade to the lastest with `gem install jirametrics` again ## JRuby How you run the commands in this section depends on how JRuby is installed: - **As a separate binary** installed alongside another Ruby: prefix each command with `jruby -S` (as shown below) so it runs under JRuby instead of your default Ruby. - **As your active Ruby**, for example through a version manager like [rvm](https://rvm.io), [rbenv](https://github.com/rbenv/rbenv), or [asdf](https://asdf-vm.com): `ruby`, `gem`, and `jirametrics` already point at JRuby, so drop the `jruby -S` prefix and run the commands exactly as in the CRuby instructions above. ### Install 1. Install [JRuby](https://www.jruby.org/download) 10 or higher. 2. Set the JAVA_OPTS environment variable as shown here. If you don't do that then the tool will hang when it tries to generate the dependency report. ``` export JAVA_OPTS="--add-opens java.base/sun.nio.ch=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED" ``` 3. Install `jirametrics` with... ``` jruby -S gem install jirametrics ``` 4. You will now invoke the tool as `jruby -S jirametrics` (or just `jirametrics` when JRuby is your active Ruby) instead of the plain `jirametrics` the rest of the instructions use. Everything else is the same. 5. Optional: If you want to use the dependency report then you'll want to install [graphviz](https://graphviz.org/download/) ### Upgrade When it's already installed, you can upgrade to the lastest with `jruby -S gem install jirametrics` again ## System package managers ### pkgsrc For platforms with binary packages available: ``` sudo pkgin -y install 'ruby*-jirametrics' ``` Or for any platform, from source, inside a pkgsrc checkout: ``` cd devel/ruby-jirametrics bmake install ``` ### Docker What if you want to deploy this as a docker image? The short answer is that we don't provide any direct support for this but there is another project that does. [Check it out.](https://github.com/magicdude4eva/docker-jirametrics) ================================================================================ # connecting_to_jira Source: https://jirametrics.org/jira/ ================================================================================ This configuration step is all about how `jirametrics` connects to your Jira instance, and this can sometimes be the hardest step. Using one of the three methods below, we've been able to connect to every Jira instance we've tried. If you encounter one that you just can't connect to, let us know. Create a file such as "jira.config". The actual name doesn't matter at this point because the main configuration file will contain a reference to it. What goes into this file depends on the type of authentication that you use with your Jira instance. This file will contain access tokens and/or cookies that are unique to you and they should be considered confidential information. Do not check them into version control or share with others. There's a reason that they're in a different file from the rest of the configuration. The code currently supports three authentication mechanisms with Jira. There are two different access tokens supported by Jira and your instance will support one or the other. If you can't get either working then fall back to the cookie method. At the time of this writing (April 2024), Jira Cloud supports the API-Token. Jira Server and Jira Data Centre support the Personal Access Token. No configuration supports both. Cookie based authentication has been deprecated in Jira Cloud and at some point will just stop working, but that was the least desirable option anyway. If you're not sure which version of Jira you're using then just try the instructions one by one and see which one works. The other kind of token just won't be an option for you to pick. # 1. Authentication with the API Token (Cloud) Navigate to [https://id.atlassian.com/manage-profile/security/api-tokens](https://id.atlassian.com/manage-profile/security/api-tokens) and create an API token. Insert that token in the jira.config like this: ```json { "url": "https://", "email": "", "api_token": "" } ``` We've seen documentation that implies that you could replace the token with your password for a Jira Server installation but we don't have an environment to test that in. Even if it does work, it would be sending your password in the clear, which is a significant security exposure. # 2. Authentication with a personal access key (Server or Data Center) Navigate to your profile in Jira and on the left menu there will be an option to manage personal access keys. If you create one then you can use this as shown. ```json { "url": "https://", "personal_access_token": "" } ``` # 3. Authentication with cookies This next option is fairly ugly and should be your last resort. It's also been deprecated in Cloud and will stop working at some point. Once you've logged in with your web browser, your browser will now have authentication cookies saved. If you go into the settings for your brower and copy them, this code can use those cookies for authentication. Generally Jira will set three different cookies and you need them all. You'll need to refresh the cookies periodically (daily?) so it's annoying. ```json { "cookies": { "": "" } } ``` # Verifying your connection Once you've created this file and referenced it from your `config.rb` (see the [Quick Start](https://jirametrics.org/quickstart/)), confirm the credentials work before downloading anything: ``` jirametrics verify ``` A successful run prints one line per configured Jira connection, naming the authenticated user. If authentication fails you'll get a message describing why, an expired token for example. This checks authentication only and downloads no issue data. Added in v3.1. ================================================================================ # Quick Start Source: https://jirametrics.org/quickstart/ ================================================================================ There are so many possible configuration options for this tool that it can be a little overwhelming to get your first report working. This page walks one path end-to-end, from nothing installed to a correct report, in a few minutes. Each step includes a way to confirm it worked before moving on. If you haven't installed JiraMetrics yet, [follow these instructions](https://jirametrics.org/install/) first. Confirm it's ready with `jirametrics --version`. This guide uses the `verify` and `boards` commands, added in v3.1, to make the setup deterministic. If you're on an earlier version, you can still follow along: skip the `verify` step, find your board id from the board's URL (see [board](https://jirametrics.org/config/project/#board)), and start with `boards: { => :default }` instead of the explicit cycle time in step 4. ## 1. Create a working directory Create a directory to hold the data you'll download and the reports you'll generate, and move into it. Both configuration files live here, and you'll run the tool from here. ``` mkdir myreports cd myreports ``` ## 2. Create the Jira credentials file Create a file called `jira_config.json` and populate it [as described here](https://jirametrics.org/jira/). On Jira Cloud it looks like this. Change it to reflect your own instance: ```json { "url": "https://improvingflow.atlassian.net", "email": "mbowler@gargoylesoftware.com", "api_token": "abcdefghijklmnopqrstuvwxyz" } ``` Now create a `config.rb` with just the connection details for the moment. We'll add your board once we've looked at it: ```ruby Exporter.configure do timezone_offset '-08:00' target_path 'target/' jira_config 'jira_config.json' end ``` Change `timezone_offset` to your own; without it the report is generated in UTC, which is almost certainly not what you want. ## 3. Verify the connection Confirm your credentials work before going any further: ``` jirametrics verify ``` A successful run prints a line like: ``` Verified https://improvingflow.atlassian.net (authenticated as Mike Bowler) ``` If it can't authenticate, it tells you why; see [Errors and how to fix them](https://jirametrics.org/troubleshooting/) for the fix. ## 4. Find your board and its columns List the boards your account can see (sorted by name): ``` jirametrics boards ``` If that's a long list, narrow it with a case-insensitive name filter, for example `jirametrics boards --name "mobile*"`. Find the one you want and note its id, then look at its columns: ``` jirametrics boards 44 ``` This prints the board's columns, left to right, and the statuses in each (with their To Do / In Progress / Done category). You need two of these column names: the one where work is genuinely **started**, and the one where it's **finished** (usually `Done`). It also names the board type (including whether a team-managed board runs on sprints), and for boards that use sprints it suggests `first_time_added_to_active_sprint` as an alternative start point when no column cleanly marks the beginning of work. ## 5. Set the cycle time and add your board Add a `standard_project` to `config.rb`, using the two column names you picked. The `require` line at the top is mandatory, because `standard_project` is a helper that lives outside the core tool. ```ruby require 'jirametrics/examples/standard_project' Exporter.configure do timezone_offset '-08:00' target_path 'target/' jira_config 'jira_config.json' standard_project name: 'Sample', file_prefix: 'sample', boards: { 44 => lambda do |_| start_at first_time_in_or_right_of_column 'In Progress' stop_at first_time_in_or_right_of_column 'Done' end } end ``` Replace `44` with your board id and the two column names with yours. This explicit start/stop is what makes your cycle time correct. There is a shortcut, `boards: { 44 => :default }`, which assumes work starts at the In Progress category and stops at Done, but that only matches about half of the boards we see, so it's worth taking the minute to set it explicitly. `first_time_in_or_right_of_column` keys off board **columns**, which is what you want when your start and stop points line up with column boundaries. Sometimes they don't: maybe work "starts" the moment a particular status is entered (not a whole column), or when a label is added, or when the item is pulled into a sprint. In those cases use a different cycle time method (for example `first_time_in_status`, `first_time_label_added`, or `first_time_added_to_active_sprint`), or, for anything the built-ins don't cover, an arbitrary block. [cycletime](https://jirametrics.org/config/cycletime/) documents the full set. **Optional:** the dependency chart included in `standard_project` needs [graphviz](https://graphviz.org/download/) (`brew install graphviz` on a mac). Nothing breaks without it; you just won't get that one chart. ## 6. Download and generate the report ``` jirametrics go ``` The most common error here is one about a missing status. If you hit it, or anything else, see [Errors and how to fix them](https://jirametrics.org/troubleshooting/). `go` downloads the data and then generates the report. You should see output something like this (you may have more lines depending on how much data there is): ``` Sample Sample Downloading all statuses Downloading board configuration for board 44 Downloading sprints for board 44 Downloading primary issues for board 44 Downloaded 1-2 of 2 issues to target/sample_issues/ Downloading linked issues for board 44 Full output from downloader in jirametrics.log Sample ``` ## 7. Open the report In the `target` directory you'll find the downloaded data plus one file with an `.html` extension, named after your `file_prefix`, so with the config above it's `target/sample.html`. Open that in a web browser and be amazed ;-) At the top of the report is a [data quality](https://jirametrics.org/data_quality/) section. Read it before trusting the charts, as it flags where the data may not mean what you'd assume. That's it for a single board. It pulls the most recent 90 days and you can refresh any time by re-running `jirametrics go`. --- ## Adding a second board A second board goes in the same `config.rb`. Run `jirametrics boards ` for it too, choose its start/stop columns, and add another `standard_project`: ```ruby require 'jirametrics/examples/standard_project' Exporter.configure do timezone_offset '-08:00' target_path 'target/' jira_config 'jira_config.json' standard_project name: 'Sample', file_prefix: 'sample', boards: { 44 => lambda do |_| start_at first_time_in_or_right_of_column 'In Progress' stop_at first_time_in_or_right_of_column 'Done' end } standard_project name: 'Board2', file_prefix: 'board2', boards: { 3 => lambda do |_| start_at first_time_in_or_right_of_column 'In Progress' stop_at first_time_in_or_right_of_column 'Done' end } end ``` --- `standard_project` is the fastest way to get running but it makes a lot of assumptions about how your boards work, and you'll likely want to build your own variation. See [`standard_project`](https://jirametrics.org/config/standard_project/) for how, and the [charts](https://jirametrics.org/charts/) for everything you can put in a report. The tool is far more customizable than `standard_project` shows. ================================================================================ # Command line usage Source: https://jirametrics.org/usage/ ================================================================================ Invoke the tool with `jirametrics [command] [options]` ## Commands The most common command you'll run is `jirametrics go`. This will download all the new issues from Jira and will then generate a report or a CSV, depending on how it's configured. If you're making changes to your configuration then you might want to skip the download step each time as that's the most time-consuming part. In that case, you can call `jirametrics download` just to download issues and `jirametrics export` to generate the output, as two separate steps. | Command | Description | |:--------|:-------| | go | Same as calling `download`, followed by `export`. It's just a shorter way to do it all at once. | | download | Download all the data from Jira | | export | Generate the respective output files. These could be CSV's or HTML reports, as specified in the configuration | | verify | Check that JiraMetrics can authenticate to your configured Jira instance(s) without downloading any data. It prints one line per connection and exits with a non-zero status if any fail, so it's a quick way to confirm your credentials and configuration before a full download. Requires v3.1 | | boards | List the boards your account can access, or show one board's columns and statuses to help you choose cycle time start/stop points. Usage: `jirametrics boards` to list them all (sorted by name), or `jirametrics boards ` for one board. Requires v3.1 | | info | Dump information about a specific issue to the terminal. This is really intended as a debugging tool for the developers but we're making it accessible for everyone. Usage: `jirametrics info ABC-123`. Requires v2.7 | ## Options | Option | Description | |:--------|:-------| | config | By default, the tool assumes that you've put your configuration in a file called config.rb. If you want to use different names then you can specify that with the `--config ` option. This is useful if you want to have multiple configuration files and only call one or another. | | name | If you have a large configuration file and only want to run a subset of the projects within it, you can use the `--name ` option. Example: `jirametrics go --name "a*"` will run all the projects starting with the letter 'a'. The name it compares against is the one specified on the project section. The same option narrows the board list for `jirametrics boards --name "a*"`, where it filters board names case-insensitively. | ================================================================================ # Top level configuration Source: https://jirametrics.org/config/ ================================================================================ Create a configuration file called `config.rb` in the same directory where you plan to run JiraMetrics. If you want to have multiple configuration files for different setups then you can override the default name in the [command line parameters](https://jirametrics.org/usage/). The contents of this file will contain some generic set up information and then specific details about each project we're looking at. This sample will give you some idea of what it might look like. Each element within that, is explained individually below. ```ruby Exporter.configure do target_path 'target' jira_config 'improvingflow.json' timezone_offset '-08:00' holiday_dates '2024-12-25', '2024-12-26' project do file_prefix 'sample' download do rolling_date_count 90 end board id: 1 do cycletime do start_at first_time_in_status_category('In Progress') stop_at still_in_status_category('Done') end end file do file_suffix '.csv' columns do write_headers true date 'Done', currently_in_status_category('Done') date 'Start', first_time_in_status_category('In Progress') string 'Type', type string 'Key', key string 'Summary', summary end end end ``` ## `target_path` The `target_path` specifies where all temporary files will be saved. We recommend that it not be the current directory as that tends to get very cluttered, very quickly. ## `jira_config` This tells JiraMetrics where to find the file that contains your [Jira configuration](https://jirametrics.org/jira/). ## `timezone_offset` Timezones in the Jira data are frequently all over the place, so in order to make the report look coherent, we have to convert them all to one common timezone. Why wouldn't we just default to the timezone of the machine that's running the report? We've found in practice that it's quite common for organizations to be spread across timezones and therefore it's common that the person running the report may be in a different timezone from the person reading it. Forcing it to a known timezone, removes that ambiguity. If you don't specify a `timezone_offset` then the report will all be generated in UTC, which is almost certainly not what you want. ## `holiday_dates` A list of non-working days. When we show the gray bars to indicate weekends, we'll also show a gray bar for any other holidays that are specified here. ```ruby holiday_dates '2024-12-25', '2024-12-26' ``` You might wonder why we don't just pull holiday information out of Jira since all of that can be entered there and it seems redundant to do it twice. The short answer is that they don't expose that information through the API. See ticket [CONFSERVER-51323](https://jira.atlassian.com/browse/CONFSERVER-51323). ================================================================================ # standard project Source: https://jirametrics.org/config/standard_project/ ================================================================================ ## `standard_project` If you choose to use `standard_project` then it can be inserted at the same place as the project declaration and you can even mix the two styles together if you want to use them both. Under the covers, `standard_project` just creates a full `project` declaration with a predefined configuration. ```ruby require 'jirametrics/examples/standard_project' Exporter.configure do target_path 'target' jira_config 'improvingflow.json' standard_project name: 'Maple', file_prefix: 'maple', boards: { 1 => :default } end ``` The `require` line is mandatory: `standard_project` is a helper that lives outside the core tool, so without it you'll get an `undefined method 'standard_project'` error. This is intended just as a quick way to get started and there is no expectation that this will do everything you want. In fact, we expect that it won't be long before you decide that this isn't good enough and you'll create your own version that does exactly what you need. Take a look at [the source code](https://github.com/mikebowler/jirametrics/blob/main/lib/jirametrics/examples/standard_project.rb) for `standard_project` to see how you could make your own. | Parameter | Description | |:--------|:-------| | `name` | The name that you're giving to this project. It will be used in the report but does not have to match any fields inside Jira itself. | | `file_prefix` | This is the file prefix that will be used for all downloaded files. This is for your use only and does not have to match to any fields inside Jira. | | `boards` | This is a hash of key/value pairs where the keys are the board ids and the values tell `jirametrics` how to determine the start and end points of every item passing through. Examples of this below. | | `ignore_issues` | It's common that we want to exclude some specific issues from the report because they're outliers and they skew the data in undesirable ways. Pass a list of ids to exclude specific issues (`ignore_issues: ['ABC123', 'ABC456']`), or pass a lambda that receives an issue and returns `true` to exclude it (`ignore_issues: ->(issue) { issue.type == 'Sub-task' }`). | | `ignore_types` | By default the tool will ignore sub-tasks and epics. This allows you to customize that. Example: `ignore_types: ['Sub-task']`. Added in v2.7 | | `starting_status` | While it's highly undesirable to move items back to the backlog, some teams have to do that and so the tool accommodates it. This status is the one that is considered the starting point. If we move back to this, we consider it in the backlog and not started. If we don't specify this parameter and this is a kanban board, then we use whatever statuses have been configured as `backlog` for that. Sprint boards don't have this concept. | | `status_category_mappings` | Allows you to specify extra statuses that have been deleted from Jira. Takes a hash of key/value pairs where the key is the status name and the value is the category. If you are specifying an ID as well as the name then use the format `"Review:4"` where the name is before the colon and the ID is after. | | `rolling_date_count` | Set the number of days of history we want to look at | | `no_earlier_than` | Restrict how far back we go by specifying one date | | `github_repos` | One or more GitHub repositories to download pull request data from. Accepts a single `'owner/repo'` string or an array of them. Requires the [GitHub CLI (`gh`)](https://cli.github.com/) to be installed and authenticated. | The value passed into `boards` is a little more complicated. It might be `:default` which assumes that we start the item when it enters the `In Progress` status category and stop it when it crosses into the `Done` status category. You might think that all boards would be configured that way and yet we find that only about half the boards we work with, do that. If it's not `:default` then it will be the same hash that you would normally pass into a [`cycletime`](https://jirametrics.org/config/project/#board) declaration. ```ruby standard_project name: 'Sample', file_prefix: 'sample', boards: { 1 => lambda do |_| start_at first_time_in_status('Refined') stop_at still_in_status_category('Done') end } ``` ================================================================================ # Configuring the Project Source: https://jirametrics.org/config/project/ ================================================================================ ## `project` The project declaration holds information about what is a project. This will often correspond to Jira's notion of a project but may not, as some companies will have many different teams working off a single Jira project. This project declaration is intended to wrap what one team will be doing and will usually (but isn't required to) have only one board. Putting an `x` in front of project will cause that project to be ignored. Example: `xproject` ```ruby Exporter.configure do target_path 'target' jira_config 'improvingflow.json' timezone_offset '-08:00' project do # ... project 1 ... end project do # ... project 2 ... end xproject do # ... ignored project ... end end ``` What if you need to have different target paths or Jira config for different projects? Each project will find the preceding settings and use those so you can redefine them at any time and the subsequent projects will use the new settings. ```ruby Exporter.configure do target_path 'target' jira_config 'improvingflow.json' project do # ... project 1 ... end target_path 'target2' jira_config 'other.json' project do # ... project 2 using different target and jira config ... end end ``` ## Naming the project A name can be passed into the project and if you have multiple projects defined, we recommend that you do. ```ruby project name: "MyName" do ... end ``` ## Setting the project id There are certain situations having to do with team-managed projects where we need to know the id of the project. If this happens and we are unable to determine the project from the board itself (often we can) then you'll get an error about having to specify the project id. You can do that like this: ```ruby project id: 5 do ... end ``` If you have both a name and an id then you can just chain them together. ```ruby project name: 'MyProject', id: 5 do ... end ``` It's rare that you'll need to specify an id so I'd ignore it until you get an error saying that it's needed. ## `file_prefix` The **file_prefix** will be used in the filenames of all files created during download or during the export. This is purely for your use and does not need to match any name inside Jira. For example, if you specify a file prefix of `sample` then it will generate a bunch of files such as `sample_board_8185_configuration.json` and `sample_meta.json`. ## `download` The **download** section contains all the information specific to the project in Jira. There can only be one of these. `rolling_date_count` indicates how many days back we're looking for files. For example, if we say `rolling_date_count 90` then we're retrieving any items that have closed in the last 90 days. We always return items that are still open, regardless of date. If this field isn't specified then we retrieve all issues that have ever been in this project and that's usually undesirable. This value is optional. `no_earlier_than` will put a boundary on the start date. If the team has changed how they work, they will often want to ignore any data before a specific date and this option allows for that. If I say `no_earlier_than '2024-10-01'` then we will only retrieve information that was after that. This option will be rarely used but when it is needed, it's very helpful. This value is optional. ```ruby download do rolling_date_count 90 no_earlier_than '2024-10-01' end ``` `github_repo` specifies one or more GitHub repositories to download pull request data from. JiraMetrics links PRs to Jira issues by looking for issue keys (e.g. `SP-123`) in the branch name, PR title, and PR description. You can call `github_repo` multiple times, or pass several repositories in a single call. Both the `owner/repo` short form and full GitHub URLs are accepted. ```ruby download do rolling_date_count 90 github_repo 'owner/repo' end ``` To include pull requests from multiple repositories: ```ruby download do rolling_date_count 90 github_repo 'owner/repo1', 'owner/repo2' end ``` Downloading GitHub pull request data requires the [GitHub CLI (`gh`)](https://cli.github.com/) to be installed and authenticated. Run `gh auth login` if you have not already done so. ## `board` The board section tells us what board we're pulling data for, and how we declare the [cycle time](https://jirametrics.org/config/cycletime/) for that board. ```ruby board id: 1 do cycletime do start_at first_time_in_status_category('In Progress') stop_at still_in_status_category('Done') end end ``` To find the board id, navigate to the respective board in your web browser and then look at the URL. There are two URL formats depending on your Jira version: * Newer URLs end in the board id, for example `.../boards/44` means the board id is `44`. * Older URLs have a `rapidView` parameter, for example `...rapidView=17...` means the board id is `17`. If there isn't a board ID in your URL then that means that this project isn't classified as a Software project in Jira and you won't have access to any of the Agile features. It's highly unlikely that you'll run into this but it is possible. JiraMetrics cannot do anything useful with projects like this. ## `discard_changes_before` If someone moves an issue back to the backlog, we can optionally pretend that it had never been started. Moving things back to the backlog after they've started is a horrible practice and yet, it's extremely common. Pass one or more status names and if the issue gets moved into any one of them, we discard any history before that point. ```ruby project name: 'foo' do discard_changes_before status_becomes: :backlog end ``` If you pass in `:backlog` AND you're using a Kanban board then that expands to the full list of statuses that are configured for your Backlog column. Scrum boards don't have the concept of backlog statuses so if you're using that, you'll need to explicitly name the statuses, for example `discard_changes_before status_becomes: ['To Do', 'Backlog']`. This belongs at the project level, as shown above. Prior to v3.0 it could also be placed inside a chart or `html_report` block, but that was removed in v3.0. See the [changelog](https://jirametrics.org/changes/). ## `file` The `file` section contains information specific to the output file we're going to create. There can be multiples of these, if we're generating multiple different files. ```ruby project do file_prefix 'sample' download do # ... end file do # ... end end ``` What the file section looks like, will depend on whether we are exporting a CSV or an HTML report. * Configuring `file` to [generate CSV files](https://jirametrics.org/project/file_csv/) * Configuring `file` to [generate an HTML report](https://jirametrics.org/project/file_html/). ## `status_category_mapping` `status_category_mapping` is there to work around a specific problem where statuses have been deleted from Jira but the Issue histories still reference that status. In this case, you'll get an error during the export, telling you to add a `status_category_mapping` and you put it inside the project section. In an English language Jira instance, the category will always be one of 'To Do', 'In Progress', or 'Done'. The status will be the name of the status that we can't find, followed by a colon and then the id, and that will be named in the error message above. ```ruby project do status_category_mapping status: 'MyNewStatus:3', category: 'In Progress' end ``` What if you only know the status name and not the id? Put that in the configuration anyway and we'll hopefully give you a better error message that might tell you what the ID is. ```ruby project do # Note that the id is missing for MyNewStatus status_category_mapping status: 'MyNewStatus', category: 'In Progress' end ``` ## `anonymize` Lastly, we can anonymize the report with the `anonymize` setting, which will strip out any confidential information before generating the report. The anonymizer only anonymizes data going into the HTML report or into the generated CSV. It does NOT anonymize the input source files so if you send them to someone else, you may still have an exposure. ```ruby project do anonymize ... end ``` ## Project specific settings The project section also has the ability to add custom settings that are used in various places. Each of these custom settings is unique and they're described in the table below. ```ruby project do ... settings['ignore_ssl_errors'] = true end ``` | Settings key | Description | |:--------|:-------| | `blocked_link_text` | If you have a link type that indicates blocked, then set this to the text that is used, such as 'Blocked by' | | `blocked_statuses` | A list of statuses that should be considered blocked. Before you start to use these, see this article on [why blocked statuses are usually a bad idea](https://blog.mikebowler.ca/2023/03/31/blocked-column/).
Example: `settings['blocked_statuses']=['Blocked']` | | `customfield_parent_links` | A list of custom_field ids that link to parent keys. If you're using Jira Advanced Roadmap then parent relationships will be set up with a custom field and this is how you set it up. | | `date_annotations` | A list of dates or times where something significant happened. See [annotations](#annotations) below | | `expedited_priority_names` | An array of priorities that will be considered to be expedited. ie ['Highest', 'Critical'] | | `flagged_means_blocked` | By default, we assume that `flagged` indicates that a ticket is blocked but some teams use `flagged` to mean something else so you can turn it off with `'flagged_means_blocked' => false`. Requires JiraMetrics v2.6 or higher | | `ignore_ssl_errors` | Set to `true` to ignore the SSL errors that are common with self-signed certificates | | `intercept_jql` | Pass a lambda that can make changes to the JQL before it's executed. If you have to use this then your Jira instance is pretty poorly configured. It's here because we have to work with instances that are. | | `jira_cloud` | The tool will normally auto-detect whether you're talking to an instance of Jira Cloud or Data Center. If it gets that wrong then you can use this property to override that. Values are `true` or `false` | | `stalled_statuses` | A list of statuses that should be considered stalled, same as blocked above. This is useful if you have queues in your workflow where the work is just sitting and waiting for someone to free up.| | `stalled_threshold_days` | The number of days of inactivity before an item becomes considered stalled. Defaults to 5 | ### Annotating charts We can annotate several of the charts with timestamps/dates where significant things happened. Declare the annotation in settings with the `date_annotations` key. These spots will be annotated on the [`cycletime_scatterplot`](https://jirametrics.org/charts/#cycletime_scatterplot), [`throughput_chart`](https://jirametrics.org/charts/#throughput_chart), [`daily_wip_by_age_chart`](https://jirametrics.org/charts/#daily_wip_by_age_chart), [`daily_wip_by_blocked_stalled_chart`](https://jirametrics.org/charts/#daily_wip_by_blocked_stalled_chart), [`daily_wip_by_parent_chart`](https://jirametrics.org/charts/#daily_wip_by_parent_chart), and [`daily_wip_chart`](https://jirametrics.org/charts/#daily_wip_chart) ```ruby "date_annotations": [ { "date": "2025-12-05T10:00:00", "label": "Stuff happened" }, { "date": "2025-12-08T10:00:00", "label": "More stuff" } ] ``` ## Excluding issues from the data set The full list of issues is made available in the `issues` variable so it's possible to do things like exclude issues we don't want. The sample below is excluding any issues that are either epics or sub-tasks. We'll often use it to exclude specific issues that we know have bad data. ```ruby issues.reject! do |issue| %w[Sub-task Epic].include? issue.type end ``` ================================================================================ # Configuring the start and end points (cycletime) Source: https://jirametrics.org/config/cycletime/ ================================================================================ ## `cycletime` The cycletime for the overall board is defined inside the [board](https://jirametrics.org/config/project/#board) declaration. Then it can be overridden in individual charts as needed. It is in the cycle time that we define the start and end points that are used for every flow calculation. The `cycletime` has two components which define when we consider the work started and when we consider it stopped. For either of `start_at` or `stop_at`, we have a variety of ways to determine the exact time. For example, if we want to start the clock when the issue enters the 'In Progress' status then we could write
`start_at first_time_in_status 'In Progress'` The one we use more often than any other is `first_time_in_or_right_of_column`. Consider if this one meets your needs before looking through all the others. The full set of options that we can use for this are below. | method | description | |---+---| | `currently_in_status` | Similar to `first_time_in_status` except that it only matches if the work is still in that status. If it has moved out of that status, it will no longer match.
`currently_in_status 'In Progress'` | | `currently_in_status_category` | Similar to `first_time_in_status_category` except that it only matches if the work is still in that category. | | `first_time_in_or_right_of_column` | Returns the first time the issue enters this column or any column the right of it. Example: `first_time_in_or_right_of_column 'In Progress'` | `first_time_in_status` | Returns the first time the issue enters one of the specified statuses. | | `first_time_in_status_category` | Returns the first time we entered a status belonging to the specified status category. Categories in an English language Jira instance will be "To Do", "In Progress" and "Done". | | `first_time_label_added` | Returns the first time a specific label has shown up on the ticket. Added in v2.8 | | `first_time_not_in_status` | Takes a list of status names and returns the first time that the issue is NOT in one of these statuses. Commonly used if there are a couple of columns at the beginning of the board that we don't want to consider for the purposes of calculating cycletime. | | `still_in_or_right_of_column` | Same as `first_time_in_or_right_of_column` except that it still has to be in one of these columns | | `still_in_status` | If an issue has ever been in one of these statuses AND is still in one of these statuses then was was the last time it entered one? This is useful for tracking cases where an item moves forward on the board, then backwards, then forward again. We're tracking the last time it entered the named status. Important: If you have two status changes in a row and both of them return true then this returns the _first_ timestamp. There are subtle cases where we want this behaviour although most of the time, you'd be better off using `currently_in_status` | | `still_in_status_category` | If an issue has ever been in one of these category AND is still in one of these category then was was the last time it entered one? This is useful for tracking cases where an item moves forward on the board, then backwards, then forward again. We're tracking the last time it entered the named category. Important: If you have two status changes in a row and both of them return true then this returns the _first_ timestamp. There are subtle cases where we want this behaviour although most of the time, you'd be better off using `currently_in_status_category` | | `first_status_change_after_created` | Returns the timestamp of the first status change after the issue was created. | | `time_created` | Returns the creation timestamp of the issue | | `first_time_visible_on_board`| returns the timestamp when the issue first became visible on the board | | `first_time_added_to_active_sprint` | If this issue will ever be in an active sprint then return the time that it was first added to that sprint, whether or not the sprint was active at that time. Although it seems like an odd thing to calculate, it's a reasonable proxy for 'ready' in cases where the team doesn't have an explicit 'ready' status. You'd be better off with an explicit 'ready' but sometimes that's not an option. | What if there aren't any built-in methods to extract the piece of data that you want? You can pass in an arbitrary bit of code that will get executed for each issue. ```ruby start_at ->(issue) { issue.get_whatever_data_you_want } ``` ### Changing the cycletime for one chart Sometimes we want to change the cycletime just for one chart. For example, I might want to have one WIP chart that shows all bugs from their creation date, not the started date. In this case, we put a cycletime inside the chart declaration and it will override the one that was set on the board. ```ruby daily_wip_chart do cycletime do start_at time_created stop_at first_time_in_or_right_of_column('Done') end end ``` ================================================================================ # Charts Source: https://jirametrics.org/charts/ ================================================================================ This page lists all the built-in charts you can put in an HTML report. _It is possible to create your own charts without forking the codebase, although the process isn't obvious or documented. If you really want to do that then [reach out](https://jirametrics.org/about/) and we can talk._ * This will become a table of contents (this text will be scrapped). ---- ## Text you can set on any chart Every chart accepts three pieces of text. Each one has a sensible default, so you only set the ones you want to change. | Setting | What it is | | --- | --- | | `header_text` | The heading above the chart | | `description_text` | The explanation shown under the heading | | `no_data_text` | What the chart shows when it has nothing to show | ```ruby cycletime_scatterplot do header_text 'How long our work takes' description_text <<-TEXT
Each dot is one completed item.
TEXT no_data_text '<%= render_header %>
Nothing finished in this period.
' end ``` ### Setting a chart to disappear Set `no_data_text` to an empty string and the chart contributes nothing at all to the report, not even its heading, when it has no data. ```ruby estimate_accuracy_chart do no_data_text '' end ``` This is useful for a chart that does not apply to every board. A sprint chart on a kanban board has nothing to say, and an empty box saying so is just noise, so `sprint_burndown` already behaves this way by default. So does `estimate_accuracy_chart` when nothing carries an estimate. ### All three are templates Each of the three is processed as [ERB](https://docs.ruby-lang.org/en/master/ERB.html) when the report is generated, so you can calculate parts of the text rather than hardcoding them. ```ruby aging_work_in_progress_chart do header_text 'Aging Work in Progress on board: <%= current_board.name %>' end ``` From inside the text you can call the chart's own methods, such as `current_board` above. You cannot reach local variables belonging to the chart's code; if you try, you will get an error naming the variable when the report is generated. Two things worth knowing: * `no_data_text` normally starts with `<%= render_header %>` so the chart still shows its heading. Leave that out and the message appears on its own. * If you use the [stitcher](https://jirametrics.org/stitcher/), `grab_by_title` matches the heading as the reader sees it, so a templated `header_text` should be referred to by its finished text rather than by the template. ---- ## `aging_work_bar_chart` This chart shows all active (started but not completed) work, ordered from oldest at the top to newest at the bottom. There are potentially three bars for each issue, although a bar may be missing if the issue has no information relevant to that. Hovering over any of the bars will provide more details. 1. The top bar tells you what status the issue is in at any time. The colour indicates the status category, which will be one of To Do, In Progress, or Done 2. The middle bar indicates blocked or stalled. 3. The bottom bar indicated expedited. ### The percentile line A vertical line marks how long your completed work actually took, drawn that many days back from today. Anything whose bars extend past the line has now been in progress longer than that percentage of everything you finished, which makes it worth a conversation. By default the line sits at the 85th percentile. Use `percentiles` to change it, or to ask for more than one. ```ruby aging_work_bar_chart do percentiles [50, 85] end ``` Several lines share the same colour, since their position is what tells them apart. Hover any of them to see which percentile it represents. Pass an empty list to draw none at all. Percentiles must be whole numbers between 0 and 100. See [why 85% is the default](https://jirametrics.org/faq/#why-85) for the reasoning behind that number. ---- ## `aging_work_in_progress_chart` For items that have started but not finished, what column are they currently in and how old are they? ```ruby aging_work_in_progress_chart ``` This chart supports the same grouping rules as Throughput Chart. See that chart for an example.. Rules options | Rule | Description | |:--------|:-------| | label|The name used for the group | | color |The color used for the group. If no color is specified then it will be randomly chosen. | | ignore |Discard this item from the dataset | ---- ## `aging_work_table` For items that are started but not finished, show a whole variety of information in a tabular format. This includes additional information not found in other charts such as the parent hierarchy, what `fix_version` this issue is in (if any), what sprints it's in (if any), due dates, etc. ### The Forecast column The Forecast column predicts how much longer an item has to go, based on how long work has historically taken to move through each remaining column on this board. By default it forecasts from the 85th percentile of that historical movement. Use `percentile` to change it. ```ruby aging_work_table do percentile 90 end ``` Note this is singular, unlike the `percentiles` setting on the charts. A forecast has to resolve to a single number of days, because that figure is also what decides whether an item with a due date is flagged as at risk, so there is nothing sensible for a list of values to mean here. A higher percentile gives a more conservative forecast. Percentiles must be whole numbers between 0 and 100. See [why 85% is the default](https://jirametrics.org/faq/#why-85). Some items cannot be forecast at all, usually because they have already been in their current column longer than the historical figure would predict. Those show a note explaining why, naming the percentile used. ---- ## `cycletime_histogram` Plots the distribution of cycle times. How many times did something complete in three days? By looking at the histogram, we can see groupings of different types of work and we can also tell how predictable the work is by how much the cycle times cluster together. ```ruby cycletime_histogram ``` This chart supports the same grouping rules as Throughput Chart. See that chart for an example.. Rules options | Rule | Description | |:--------|:-------| | label|The name used for the group | | color |The color used for the group. If no color is specified then it will be randomly chosen. | | ignore |Discard this item from the dataset | ### Choosing which percentiles the statistics show Below the chart is a statistics table with a column for each percentile, and a short note explaining what each one is useful for. By default you get the 50th, 85th and 98th: the median, the number most people use for a service level expectation, and a sense of the worst case. Use `percentiles` to ask for something else. ```ruby cycletime_histogram do percentiles [50, 90] end ``` The explanations follow whatever you configure, so asking for the 90th tells you what the 90th is good for rather than describing a column that isn't there. Pass an empty list to drop the percentile columns entirely. Percentiles must be whole numbers between 0 and 100, and anything else is rejected when the config is read rather than quietly producing a wrong table. See [why 85% is the default](https://jirametrics.org/faq/#why-85) for the reasoning behind that particular number. ---- ## `cycletime_scatterplot` Plots the cycle time (y axis) against the date that the work completed (x axis). ```ruby cycletime_scatterplot ``` You can customize this report with `grouping_rules` as shown below. | Rule | Description | |:--------|:-------| | label|The name used for the group | | color |The color used for the group. If a colour isn't set then it will be randomly chosen. | | ignore |Discard this item from the dataset | | percentiles |Which percentile lines to draw for this group, overriding the chart default. See [percentile lines](#choosing-which-percentile-lines-to-draw-with-percentiles). | Example ```ruby cycletime_scatterplot do grouping_rules do |issue, rules| # Put all data into groups by type. Use the name of the # type as the label for the group rules.label = issue.type if issue.type == 'Story' # Set the color of stories to be green rules.color = 'green' else issue.type == 'Spike' # Ignore spikes rules.ignore else rules.color = 'yellow' end end end ``` ### Choosing which percentile lines to draw with `percentiles` The chart draws horizontal reference lines at chosen percentiles of the data. If a line sits at 12 days then that percentage of the work completed in 12 days or less. One line is drawn across the whole data set, in its own colour, and one is drawn for each group in that group's colour. By default the chart draws the 85th percentile, which is a reasonable proxy for "most". Use `percentiles` to ask for something else. ```ruby cycletime_scatterplot do percentiles [50, 85, 98] end ``` The 50th tells you the typical case, the 85th is the usual choice for setting a service level expectation, and the 98th shows you the worst case you should plan for. Asking for more lines makes the chart busier, and how busy is too busy is your call. Pass an empty list to switch the lines off completely. ```ruby cycletime_scatterplot do percentiles [] end ``` Hover over any line to see which line it is and what its value is, for example `Story 85% at 12 days`. The line drawn across the whole data set names itself `All items`, since it has no legend entry of its own. Values also appear in the legend entry for each group, so `Story (85% at 12 days)` tells you the 85th percentile for stories is 12 days without having to hover at all. #### Different percentiles for different groups The chart level setting does double duty: it defines the lines drawn across the whole data set, and it supplies the default for every group. A group can override that default by setting `percentiles` in its `grouping_rules` block. ```ruby cycletime_scatterplot do percentiles [85] grouping_rules do |issue, rules| rules.label = issue.type # Bugs get a second line so we can see the typical case as well rules.percentiles = [50, 85] if issue.type == 'Bug' # Spikes are too variable for a percentile to mean anything rules.percentiles = [] if issue.type == 'Spike' end end ``` A group that never sets `percentiles` inherits the chart default. Setting it to an empty list is different from not setting it at all: the empty list means this group shows no lines, while leaving it alone means "use whatever the chart says". Percentiles must be whole numbers between 0 and 100. Anything else is rejected when the config is read, rather than quietly producing a misleading chart. The same option is available on [`pull_request_cycle_time_scatterplot`](#pull_request_cycle_time_scatterplot). ### Showing trend lines with `show_trend_lines` Off by default. When switched on, the chart adds a dashed line for each group, in that group's colour, fitted through that group's dots. The slope tells you whether cycle times have been getting longer or shorter across the period shown. ```ruby cycletime_scatterplot do show_trend_lines end ``` It is a straight-line fit, so it describes the window you are looking at rather than predicting beyond it, it cannot show a trend that changed direction partway through, and a handful of unusually long items will tilt it noticeably. A group needs at least three items before a line is drawn at all, since two will always fit a straight line perfectly and so tell you nothing. Even then, nothing checks how well the line fits, so a scattered cloud with no real trend in it still gets a confident looking line through it. The chart says as much underneath itself when the lines are turned on. The same option is available on [`pull_request_cycle_time_scatterplot`](#pull_request_cycle_time_scatterplot). ### Capping the y axis with `cap_y_axis` A handful of very long-running items can stretch the y axis so far that the bulk of your work is squashed into a thin band at the bottom and becomes hard to read. Making the chart taller does not help, because the problem is the range, not the height. `cap_y_axis` caps the y axis at a percentile of the data so you can zoom in on the cases that matter most. Items above the cap are not discarded. They move into a distinct band above an axis break (drawn as a double line), shown as up arrows in their group colour with a label counting how many there are. Hovering an arrow still shows its real cycle time. Everything below the break expands to fill the readable area. ```ruby cycletime_scatterplot do cap_y_axis percentile: 90 end ``` The option is off by default; without it the chart shows every item, auto-scaled as before. Called with no argument, `cap_y_axis` defaults to the 98th percentile, which keeps all but the genuine long tail on scale. Capping only changes what you see, not the numbers: the [percentile lines](#choosing-which-percentile-lines-to-draw-with-percentiles) are always calculated from the full data set. The same option is available on [`pull_request_cycle_time_scatterplot`](#pull_request_cycle_time_scatterplot). ---- ## `cumulative_flow_diagram` A Cumulative Flow Diagram (CFD) shows how work accumulates across board columns over time. Each coloured band represents a workflow stage. A widening band means work is piling up in that stage - a bottleneck. Parallel band edges indicate smooth flow. Dashed lines and hatched regions indicate periods where an item moved backwards through the workflow. ```ruby cumulative_flow_diagram ``` The chart overlays two trend lines showing the **arrival rate** (how fast work enters the system) and the **departure rate** (how fast it leaves). Moving the mouse over the chart shows a Little's Law triangle at that point in time, labelled with **WIP** (items in progress), **cycle time** (average days to complete), and **throughput** (items per day). A checkbox above the chart toggles between the triangle and the normal data tooltips. You can customise the chart using `column_rules` and colour options: ```ruby cumulative_flow_diagram do column_rules do |column, rule| rule.color = '#4a90d9' if column.name == 'In Progress' rule.label = 'WIP' if column.name == 'In Progress' rule.label_hint = 'Items actively being worked on' if column.name == 'In Progress' rule.ignore if column.name == 'Review' end arrival_rate_line_color 'rgba(255,100,50,0.9)' departure_rate_line_color '#80cbc4' triangle_color '#ffff00' end ``` **`column_rules`** | Rule | Description | |:--------|:-------| | color | The colour used for the column band. Accepts any CSS colour string. If not set, a colour is chosen automatically. Note: only `#rrggbb` hex values will have the band fill automatically lightened; other formats are used as-is for both the border and fill. | | label | Overrides the column name shown in the chart legend. | | label_hint | Tooltip text shown when hovering over the legend item for this column. | | ignore | Exclude this column from the chart entirely. | **Colour options** | Option | Default | Description | |:--------|:-------|:-------| | `arrival_rate_line_color` | `rgba(255,138,101,0.85)` | Colour of the arrival rate trend line and its label. Pass `nil` to hide the line entirely. | | `departure_rate_line_color` | `rgba(128,203,196,0.85)` | Colour of the departure rate trend line and its label. Pass `nil` to hide the line entirely. | | `triangle_color` | dark/light pair | Colour of the Little's Law triangle sides. | To hide one or both trend lines: ```ruby cumulative_flow_diagram do arrival_rate_line_color nil # hide arrival trend line departure_rate_line_color nil # hide departure trend line end ``` See also, this article on [how to read a cumulative flow diagram](https://blog.mikebowler.ca/2026/03/27/cumulative-flow-diagram/) ---- ## `daily_view` This report lists all the aging items in order of importance. We find that many teams aren't clear on what order they should discuss items in their daily meeting (standup / scrum / etc) so this chart lays them out in the correct order, sorted first by priority and then by age within that priority level. Most important at the top and least at the bottom. The expectation is that you can use just this view during your daily meeting, without looking at the Jira board itself. We're still experimenting with exactly what information needs to be presented in order to meet that goal, so this may change over time. To understand the motivation for this chart, see [this article](https://blog.mikebowler.ca/2025/07/14/jirametrics/). ---- ## `daily_wip_by_age_chart` For each day in the period, how many items were in progress? Items are colour coded based on how long they've been in progress. ```ruby daily_wip_by_age_chart ``` For documentation on options for this chart, see [`daily_wip_chart`](#daily_wip_chart) ---- ## `daily_wip_by_blocked_stalled_chart` For each day in the period, how many items are blocked (Flagged in Jira terms) or stalled (no status changes in the last five days)? ```ruby daily_wip_by_blocked_stalled_chart ``` For documentation on options for this chart, see [`daily_wip_chart`](#daily_wip_chart) ---- ## `daily_wip_by_parent_chart` Grouping the WIP by the parent ticket. This is useful to see if we're focused on more strategic goals (small number of epics) or whether our focus is scattered. ```ruby daily_wip_by_parent_chart ``` See [this article](https://blog.mikebowler.ca/2025/01/29/wip-by-parent/) for more details on what we can learn from this chart. For documentation on options for this chart, see [`daily_wip_chart`](#daily_wip_chart) ---- ## `daily_wip_chart` The daily WIP charts above are just customized versions of the more generic `daily_wip_chart`. If you want to build your own, you can do that with code like the example below. This example groups the daily WIP by the parent of the ticket in progress. ```ruby daily_wip_chart do header_text 'Daily WIP by Parent' description_text <<-TEXT How much work is in progress, grouped by the parent of the issue. This will give us an indication of how focused we are on higher level objectives. If there are many parent tickets in progress at the same time, either this team has their focus scattered or we aren't doing a good job of splitting those parent tickets. Neither of those is desirable. TEXT grouping_rules do |issue, rules| rules.label = issue.parent&.key || 'No parent' rules.color = 'white' if rules.label == 'No parent' end end ``` | Grouping rule | Description | | --- | --- | | label | The text description that will be used for this grouping | | color | The colour that will be used in the chart for this grouping | | highlight | True if this item should be highlighted (drawn differently) | | issue_hint | Extra text that will be visible in the tooltip | | label_hint | Optional tooltip text shown when hovering over the legend item. | ---- ## `dependency_chart` Jira gives you the ability to link issues. So you can say that one issue depends on another or one blocks another. This visualizes all of those relationships. ```ruby dependency_chart ``` Note that this requires graphviz to be installed. See the [GraphViz](https://graphviz.org/download/) website for installation instructions. If GraphViz can't be found then the report will still be generated but this paticular chart will be skipped. You can customize this chart with two different kinds of rules. One to describe the actual issues themselves and the other to describe the links between issues. ### `issue_rules` To customize the individual issues. | Rule | Description | |:--------|:-------| | color |The color used for the group | | ignore |Discard this item from the dataset | Example ```ruby dependency_chart do # Set custom colours based on the type of the object. # Note that this sample just uses the same colour scheme # that you get by default so you'll probably want to change # the colour values if you're using this. issue_rules do |issue, rules| rules.color = case issue.type when 'Story' '#90EE90' when 'Task' '#87CEFA' when 'Bug', 'Defect' '#ffdab9' when 'Epic' '#fafad2' else '#dcdcdc' end # Ignore sub tasks rules.ignore if issue_type == 'Sub-task' end end ``` ### `link_rules` To customize the links between issues | Rule | Description | |:--------|:-------| | line_color |The color used for the group | | ignore |Discard this item from the dataset | | merge_bidirectional keep: 'outward' | If there are bidirectional links (ie A depends on B and B also depends on A) then only draw one of the two lines | | use_bidirectional_arrows | If there are bidirectional links then display an arrow at both ends of the line. | ```ruby dependency_chart do link_rules do |link, rules| case link.name when 'Cloners' # We don't want to see any clone links at all. rules.ignore when 'Blocks' # For blocks, by default Jira will have links going both # ways and we want them only going one way. Also make the # link red. rules.merge_bidirectional keep: 'outward' rules.line_color = 'red' when 'Sync' # For sync, also only show one link but this time put # arrows at both ends rules.merge_bidirectional keep: 'outward' rules.use_bidirectional_arrows end end end ``` ---- ## `estimate_accuracy_chart` Graphs the estimates (y axis) against the actual cycle time of the item. It's useful to be able to see how much correlation there is between the estimates and the actual time it took. By default, it uses _story points_ for the estimate although that can be configured as seen below. There is never any correlation between the two, which begs the question _"why we even do story point estimates if they're never accurate?"_ More on that [here](https://blog.mikebowler.ca/2023/07/08/per-story-estimates/). ```ruby estimate_accuracy_chart ``` What if you don't use the _story point_ field and use something custom like TShirt sizes? You can specify that with the yaxis ```ruby estimate_accuracy_chart do y_axis(sort_order: %w[Small Medium Large], label: "TShirt Sizes") do |issue, started_time| issue.raw['fields']['custom_field_34'] end end ``` **Note:** In this example, _custom_field_34_ is meant to show what's possible. It's almost certainly not going to be called that in your instance. You have to find what field is holding the value you need. | Parameters | Description | |:--------|:-------| | sort_order|All the possible options in the order you want to see them displayed. Bottom to top. | | label |The label you want displayed on the axis | | block |The code that will extract the value from the issue object. This is custom to your setup | ---- ## `expedited_chart` This chart shows how many items are expedited and how long they've been that way. Configure what it means for an item to be expedited through the `expedited_priority_names` key in [project specific settings](https://jirametrics.org/config/project/#settings) ```ruby expedited_chart ``` ---- ## `pull_request_cycle_time_histogram` Plots the distribution of pull request cycle times. How many PRs closed in one day? Two days? This makes it easy to see how predictable PR turnaround is and whether there are outliers worth investigating. These charts require GitHub pull request data to be downloaded. Add `github_repo` to your [`download` block](https://jirametrics.org/config/project/#download) and ensure the [GitHub CLI (`gh`)](https://cli.github.com/) is installed and authenticated. JiraMetrics links PRs to issues by searching for Jira issue keys in the branch name, PR title, and PR description. ```ruby pull_request_cycle_time_histogram ``` The unit used for the x axis defaults to `:days` but can be changed to `:hours`, `:minutes`, or `:hours24`. `:days` counts calendar days from midnight to midnight, so a PR opened and closed either side of midnight counts as two days. `:hours24` instead counts elapsed 24-hour periods measured from the clock, so that same PR counts as one. For `:hours`, `:minutes`, and `:hours24`, any partial unit rounds up - a PR open for 20 minutes shows as 1 hour, not 0. ```ruby pull_request_cycle_time_histogram do cycletime_unit :hours end ``` You can also customize the grouping with `grouping_rules`. ```ruby pull_request_cycle_time_histogram do cycletime_unit :hours grouping_rules do |pull_request, rules| rules.label = pull_request.repo rules.color = 'green' end end ``` | Grouping rule | Description | |:--------|:-------| | label | The name used for the group | | color | The color used for the group. If no color is specified then it will be randomly chosen. | | ignore | Discard this item from the dataset | | percentiles | Which percentile lines to draw for this group, overriding the chart default. | This chart also supports [`cap_y_axis`](#capping-the-y-axis-with-cap_y_axis) and [`percentiles`](#choosing-which-percentile-lines-to-draw-with-percentiles), which behave exactly as they do on the cycle time scatterplot. ---- ## `pull_request_cycle_time_scatterplot` Plots the cycle time (y axis) against the date the pull request was closed (x axis), where cycle time is measured from when the PR was opened to when it was closed. By default, items are grouped by repository. These charts require GitHub pull request data to be downloaded. Add `github_repo` to your [`download` block](https://jirametrics.org/config/project/#download) and ensure the [GitHub CLI (`gh`)](https://cli.github.com/) is installed and authenticated. JiraMetrics links PRs to issues by searching for Jira issue keys in the branch name, PR title, and PR description. ```ruby pull_request_cycle_time_scatterplot ``` The unit used for the y axis defaults to `:days` but can be changed to `:hours`, `:minutes`, or `:hours24`. `:days` counts calendar days from midnight to midnight, so a PR opened and closed either side of midnight counts as two days. `:hours24` instead counts elapsed 24-hour periods measured from the clock, so that same PR counts as one. For `:hours`, `:minutes`, and `:hours24`, any partial unit rounds up - a PR open for 20 minutes shows as 1 hour, not 0. ```ruby pull_request_cycle_time_scatterplot do cycletime_unit :hours end ``` You can also customize the grouping with `grouping_rules`. ```ruby pull_request_cycle_time_scatterplot do cycletime_unit :hours grouping_rules do |pull_request, rules| rules.label = pull_request.repo rules.color = 'green' end end ``` | Grouping rule | Description | |:--------|:-------| | label | The name used for the group | | color | The color used for the group. If no color is specified then it will be randomly chosen. | | ignore | Discard this item from the dataset | | percentiles | Which percentile lines to draw for this group, overriding the chart default. | This chart also supports [`cap_y_axis`](#capping-the-y-axis-with-cap_y_axis) and [`percentiles`](#choosing-which-percentile-lines-to-draw-with-percentiles), which behave exactly as they do on the cycle time scatterplot. ---- ## `sprint_burndown` Displays all the sprint burndowns that happened during this period. By default, this renders two charts - the top one is burndown by story points and the bottom one is burndown by story count. If you only want one or the other then you can customize that. ```ruby # Generate both burndowns sprint_burndown # Generate only the story point burndown sprint_burndown :points_only # Generate only the story count burndown sprint_burndown :counts_only ``` ---- ## `throughput_chart` A line chart showing how many items completed each week (Monday to Sunday) ```ruby throughput_chart ``` By default, this splits data across issue types and also shows a totals line. Rules options | Rule | Description | |:--------|:-------| | label | The name used for the group | | color | The color used for the group. If no color is specified then it will be randomly chosen. | | label_hint | Optional tooltip text shown when hovering over the legend item. Also used in the data point tooltip as "N items closed with _label_hint_ between ...". | | ignore | Discard this item from the dataset | | last_day_of_period | The last day of the time bucket this item belongs to. When set for any issue, the chart switches from fixed weekly periods to the custom periods you define here. Accepts a `Date` or a `String` in `'YYYY-MM-DD'` format. Items whose `last_day_of_period` is not set are excluded from the chart. | The `last_day_of_period` rule is useful when you want to group throughput by calendar months (which vary in length), sprints, or any other irregular boundaries rather than the default Monday–Sunday weeks. Each unique `last_day_of_period` value becomes one data point on the x-axis. Example - grouping by calendar month: ```ruby throughput_chart do grouping_rules do |issue, rules| rules.label = issue.type rules.color = color_for(type: issue.type) # Assign the issue to the last day of its completion month stop_date = issue.started_stopped_dates.last rules.last_day_of_period = Date.new(stop_date.year, stop_date.month, -1) if stop_date end end ``` Example - grouping by issue type (default weekly buckets): ```ruby throughput_chart do grouping_rules do |issue, rules| # Put all data into groups by type. Use the name of the # type as the label for the group rules.label = issue.type if issue.type == 'Story' # Set the colour of stories to be green rules.color = 'green' else issue.type == 'Spike' # Ignore spikes rules.ignore else rules.color = 'yellow' end end end ``` ---- ## `wip_by_column_chart` Shows how much time each board column has spent at different WIP (Work in Progress) levels over the reporting period. Each column on the x axis is a board column; each row on the y axis is a WIP level (the number of items in that column at the same time). A horizontal bar at a given intersection shows what percentage of the total time that column spent at that WIP level - a wider bar means more time was spent there. Dashed lines show the minimum and maximum WIP limits configured on the board. Columns with no activity (always at WIP 0) are trimmed from both ends automatically. ```ruby wip_by_column_chart ``` Optionally enable WIP limit recommendations, which analyse the historical data and suggest adjustments: ```ruby wip_by_column_chart do show_recommendations end ``` When `show_recommendations` is enabled, the chart calculates the 85th-percentile WIP level for each column - the WIP at which 85% of the total column time is accounted for - and draws a recommendation line on the chart. Below the chart, a plain-language summary is shown for each column where an adjustment is warranted: - _"Add a WIP limit to column 'X' - suggested maximum: N"_ - no limit is currently set - _"Lower the WIP limit for 'X' from M to N"_ - the current limit is higher than the data suggests is needed - _"Raise the WIP limit for 'X' from M to N"_ - the team is regularly working above the current limit - _"Almost nothing passes through column 'X'. Do we still need it?"_ - 85% of the time this column has zero items ---- ## `throughput_by_completed_resolution_chart` A variant of [`throughput_chart`](#throughput_chart) that groups completed items by the Jira status and resolution they had when they were done, rather than by issue type. This makes it easy to see how many items completed in each resolution category (e.g. Done/Fixed vs Done/Won't Fix). Hovering over a legend item shows the exact status name, status ID, and resolution. Hovering over a data point shows the count of items closed with that status/resolution combination for that week. ```ruby throughput_by_completed_resolution_chart ``` This chart supports the same `grouping_rules` override as `throughput_chart` if you want to customise the grouping. ================================================================================ # Configuring a File to output an HTML report Source: https://jirametrics.org/project/file_html/ ================================================================================ # `file` The `file` section contains information about a specific file that we want to export. This page explains how to use it to export an HTML report. If you looking for the data in CSV then [click over here](https://jirametrics.org/project/file_csv/) ```ruby file do file_suffix '.html' html_report do # List of all the charts we want to include, in the order we want to see them in the report cycletime_scatterplot cycletime_histogram throughput_chart end end ``` ## `file_suffix` Define the suffix that will be used for the generated file. If not specified, it defaults to `.csv` so when generating an HTML report, you probably want to set it. ```ruby file_suffix '.html' ``` ## `discard_changes_before` As of v3.0, `discard_changes_before` is configured at the **project** level, not inside the `file` or `html_report` block. See [`discard_changes_before`](https://jirametrics.org/config/project/#discard_changes_before). ## `html_report` The `html_report` block contains all the individual charts that you want included in the report. The charts will show up in the report in the order that they are defined here and it's ok to have the same chart show up multiple times with different options set. ### `board_id` If you specified multiple boards in the `project` section then you'll need to specify which one of those is in use for this report. ```ruby board_id: 1 ``` ### Charts There are many charts that you can add to the report and [all charts are documented here](https://jirametrics.org/charts/). You can add them here in the order you want to see them in the report. ```ruby html_report do cycletime_scatterplot cycletime_histogram throughput_chart end ``` ## Styling the report Out of the box, the report supports light mode and dark mode. It will obey whatever the Operating System tells it about whether you're in light mode or dark mode and will adjust accordingly. There is nothing you need to configure to make this happen. If you decide that you want to customize the colours or general styling then you can override the [default CSS](https://github.com/mikebowler/jirametrics/blob/main/lib/jirametrics/html/index.css) by creating your own CSS file that will be loaded after the default one. This site is not a tutorial on CSS so all I'll say is that the colours we use are set in CSS variables and you can easily override them. Inside your project declaration, you'll want to add a setting for `include_css`, where you specify the filename of your custom css. ```ruby project name: 'foo' do setting['include_css'] = './my_custom_css.css' end ``` ### The dependency chart The dependency chart used to be the exception here, because the tool that draws it, [Graphviz](https://graphviz.org), knows nothing about CSS. Its colours are now variables like everything else. Each type has a colour for the box and a colour for the text written inside it. They come in pairs, because most of the boxes are dark with white text and some are light with black. ```css :root { --dependency-chart-story-color: #015C41; --dependency-chart-story-label-color: white; --dependency-chart-task-color: #56B4E9; --dependency-chart-task-label-color: black; --dependency-chart-bug-color: #783200; /* also used for Defect */ --dependency-chart-bug-label-color: white; --dependency-chart-epic-color: #F0E442; --dependency-chart-epic-label-color: black; --dependency-chart-spike-color: #762A58; --dependency-chart-spike-label-color: white; --dependency-chart-label-color: black; /* text on a box you coloured yourself */ --dependency-chart-link-color: gray; /* the lines between boxes, and their labels */ } ``` An issue type not in that list reuses one of those same five pairs rather than coming from [the fallback palette](#the-fallback-palette), so two uncommon types can end up sharing a colour. Every node names its own type, so you can still tell them apart. The defaults are chosen to work well for people who are colour blind, so please keep that in mind if you replace them. **If you change a box colour, change its label colour to match.** That is the one thing worth being careful about here: these colours sit behind text rather than beside it, so a dark box needs white text and a light box needs black. Nothing checks this for you, and getting it wrong makes the node unreadable. For the same reason they need no separate dark mode value, since the box is its own background. `--dependency-chart-link-color` does have one, because the lines sit on the page rather than inside a box. ### The fallback palette Some things on a chart need to be told apart without any particular colour being called for, such as one series per epic when you have not said which colour each epic should be. Those come from a fallback palette defined as `--palette-color-1` upwards. It is the [Okabe-Ito palette](https://jfly.uni-koeln.de/color/), chosen because its colours stay distinguishable to people with colour vision deficiency, so please keep that in mind if you replace them. You can override any slot the same way you override any other colour. You can also extend the palette simply by defining the next number, and it will be used; the number of slots is read from the CSS rather than fixed in the code. ```css :root { --palette-color-3: #117733; /* replace a slot */ --palette-color-8: #882255; /* add a new one */ } ``` If you need a *specific* colour for a *specific* thing, configure it on the chart rather than relying on which palette slot that thing happens to be given. The slot a series gets depends on how many other series were drawn before it. ### Reverting to the legacy colour scheme The default colours were updated to improve accessibility for people with colour vision deficiencies (colour blindness). If you prefer the original colour scheme, you can opt out by using the [legacy_colors.css](https://github.com/mikebowler/jirametrics/blob/main/lib/jirametrics/html/legacy_colors.css) file that ships with jirametrics. Save a copy of that file alongside your config file, then point `include_css` at it: ```ruby project name: 'foo' do setting['include_css'] = './legacy_colors.css' end ``` The file covers both light mode and dark mode colours. ================================================================================ # Configuring a File to output data Source: https://jirametrics.org/project/file_csv/ ================================================================================ # `file` The `file` section contains information about a specific file that we want to export. This page explains how to use it to export CSV files. If you looking for the HTML report then [click over here](https://jirametrics.org/project/file_html/) ```ruby file do file_suffix '.csv' # This is a typical configuration for the team dashboard at FocusedObjective.com columns do write_headers true date 'Done', currently_in_status_category('Done') date 'Start', first_time_in_status_category('In Progress') string 'Type', type string 'Key', key string 'Summary', summary end end ``` ## `file_suffix` Define the suffix that will be used for the generated file. If not specified, it defaults to `.csv`. ## `only_use_row_if` This is a hack to exclude rows that we don't want to see in the export. This will likely be deprecated at some point in the future when we figure out a better approach. For example, sometimes we only want to write a row if it has either a start date or an end date or both. We could use this to exclude the row unless one of those values is present. ```ruby file do only_use_row_if do |row| row[0] || row[1] end end ``` ## `columns` The `columns` block provides information about the actual data that will be exported. Within the columns declaration, we can have a number of options. ### `write_headers` This indicates whether we want a header row in the output or not. The default is false. ### Type specific values These are `date` or `string` This will output a value of the appropriate type into a column of the output file. The first parameter is the name of the column and the second is a method that will be called on the Issue class. Methods that are frequently used here are any of methods that you would have used with `start_at` or `stop_at` in the board configuration. Also the items below. | `key` | The Jira issue number | | `type` | The issue type | | `summary` | The issue description | | `url` | The issue URL. | `blocked_percentage` | Takes two of the above date methods (first for the start time and second for the end time) and then calculates the percentage of time that this issue was marked as blocked (flagged in Jira parlance).| If there isn't already a method to do what you want, you can specify some code to calculate that value for yourself. ```ruby columns do string 'sprint_count', ->(issue) { issue.get_whatever_data_you_want } end ``` ### Accessing custom fields Often teams have created custom fields in their Jira instance and they want to retrieve that data. We start with one of `date` or `string` and then specify some logic to pull that custom field out of the issue. Unfortunately, this requires us to reach directly into the JSON response that was returned for this issue, but the good news is that it isn't too difficult. We start with a declaration like this: ```ruby columns do string 'points', ->(issue) { issue.raw['fields']['custom_field123'] } end ``` `issue.raw` returns the raw JSON that had been returned from the Jira instance. From there, you can reach in and pull out the values you care about. Custom fields are typically stored under 'fields' in the JSON. If you look under the target directory, you'll find one or more directories that end in `_issues` and within one of those directories, you'll find the raw JSON responses that Jira had given us. Opening one of these in any text editor will let you see the full response. The tricky part is figuring out which custom field is the one that you care about. It's tricky because it won't have the same name as what you see in the Jira user interface. Instead, you'll have to search for possible values to see what that has been set to. For example, if I have a custom field where one possible value is 'Bob' then I search the JSON for 'Bob' and that's the field I care about. ### `column_entry_times` This will autogenerate multiple columns based on the columns found on your board and will put an entry date in each of those columns. This is useful for tools like Actionable Agile that need entry times per column. Note that to use this option, you must have specified a board_id in the project. ```ruby # This is typical configuration for the Actionable Agile tool columns do write_headers true string 'ID', key string 'link', url string 'title', summary column_entry_times end ``` ================================================================================ # standard project Source: https://jirametrics.org/config/aggregated_project/ ================================================================================ ## `aggregated_project` Same idea as [`standard_project`](https://jirametrics.org/config/standard_project/) except that instead of looking at a single project, we're aggregating a bunch of them together to look at a higher level view. ```ruby Exporter.configure do target_path 'target' jira_config 'improvingflow.json' standard_project name: 'Maple', file_prefix: 'maple', boards: { 1 => :default } standard_project name: 'Pine', file_prefix: 'pine', boards: { 2 => :default } aggregated_project name: 'forest', project_names: ['Maple', 'Pine'] end ``` This is intended just as a quick way to get started and there is no expectation that this will do everything you want. In fact, we expect that it won't be long before you decide that this isn't good enough and you'll create your own version that does exactly what you need. Take a look at [the source code](https://github.com/mikebowler/jirametrics/blob/main/lib/jirametrics/examples/aggregated_project.rb) for `aggregated_project` to see how you could make your own. | Parameter | Description | |:--------|:-------| | `name` | The name that you're giving to this project. It will be used in the report but does not have to match any fields inside Jira itself. | | `project_names` | This is list of project names that will be included in the aggregated report. All of these names must have already been declared using either `project` or `standard_project` | ================================================================================ # Errors and how to fix them Source: https://jirametrics.org/troubleshooting/ ================================================================================ This page maps the errors you're most likely to hit to their cause and the fix, roughly in the order they show up during setup. If you just want a working setup from scratch, start with the [Quick Start](https://jirametrics.org/quickstart/) and come back here when something breaks. Two commands are useful diagnostics throughout (both added in v3.1): - `jirametrics verify` checks that your credentials authenticate, without downloading any data. - `jirametrics boards` lists your boards; `jirametrics boards ` shows a board's columns and statuses. ## Installation ### `command not found: jirametrics` **Cause:** the gem installed into a Ruby that isn't first on your `PATH`. This is common when Ruby was installed with Homebrew or a version manager. **Fix:** see the `PATH` note in the [installation instructions](https://jirametrics.org/install/), then confirm with `jirametrics --version`. ## Connecting to Jira ### `The request was not authorized. Verify that your authentication token hasn't expired` **Cause:** HTTP 401. The API token (Cloud) or personal access token (Server/Data Center) is wrong, expired, or was deleted on the server. **Fix:** recreate the token, update the file referenced by `jira_config`, and run `jirametrics verify` to confirm. See [Connecting to Jira](https://jirametrics.org/jira/). ### `Jira returned 503 (Service Unavailable) ...` **Cause:** a Jira outage, or (for a free Cloud instance) the instance was deactivated after a period of inactivity. **Fix:** check your Jira status/subscription and retry. If it's a free instance that went dormant, reactivate it. ### An error about being rate limited **Cause:** either you really have hit the instance too often, or (misleadingly) your token was deleted on the server and Jira returned a rate-limit message instead of a clear auth error. **Fix:** wait and retry; if it persists, recreate your token and run `jirametrics verify`. More detail in the [FAQ](https://jirametrics.org/faq/#rate-limited). ### `Must specify URL in config` **Cause:** the Jira config JSON is missing a `url` (or it's malformed). **Fix:** make sure the file referenced by `jira_config` has a valid `url`. See [Connecting to Jira](https://jirametrics.org/jira/). ## Configuration ### `standard_project is an example rather than part of jirametrics` **Cause:** your `config.rb` uses `standard_project` but never required it. The same applies to `aggregated_project` and anything else under `examples`. **Fix:** add `require 'jirametrics/examples/standard_project'` as the first line of `config.rb`. The error message names the exact line to add. See [standard_project](https://jirametrics.org/config/standard_project/). Older versions reported this as `undefined method 'standard_project'`. ### `Cannot find configuration file "config.rb"` **Cause:** you're running from a directory that has no `config.rb`, or the `--config` path is wrong. **Fix:** run from the directory that holds `config.rb`, or pass `--config `. ### `Warning: The history for issue X references a status ("Name":id) that can't be found ...` **Cause:** a status that used to exist has been deleted in Jira, so its category (To Do / In Progress / Done) can't be looked up. **Fix:** declare the missing status's category. With `standard_project`, use the `status_category_mappings` parameter (note the trailing `s`); with the full DSL, use `status_category_mapping`. Full explanation in [FAQ #1](https://jirametrics.org/faq/#q1). ### `discard_changes_before: Status "X" not found` **Cause:** the status you named for [`discard_changes_before`](https://jirametrics.org/config/project/#discard_changes_before) doesn't exist on the board. **Fix:** check the spelling. `jirametrics boards ` lists the real status names, shown as `"name":id`. ## Downloading and exporting ### `No data found. Must do a download before an export` **Cause:** you ran `jirametrics export` before ever downloading. **Fix:** run `jirametrics download` first (or `jirametrics go`, which downloads and then exports in one step). ## Interpreting the report ### Cycle times look wrong, or items are missing from a chart **Cause:** the cycle time start/stop points don't match how your board actually works, most often because the config uses `boards: { N => :default }`, which only fits about half of boards. **Fix:** run `jirametrics boards ` to see your columns, then set explicit start and stop points with `first_time_in_or_right_of_column`. See [cycletime](https://jirametrics.org/config/cycletime/). The [data quality](https://jirametrics.org/data_quality/) section at the top of every report also flags many of these cases. ### Parent issues aren't linked correctly **Cause:** Jira stores the parent in an instance-specific custom field that JiraMetrics can't guess. **Fix:** set `customfield_parent_links`. See the [FAQ](https://jirametrics.org/faq/#parent_key). ================================================================================ # faq Source: https://jirametrics.org/faq/ ================================================================================ * This will become a table of contents (this text will be scrapped). ---- # Errors For a concise map of the errors you're most likely to hit, with the cause and fix for each, see [Errors and how to fix them](https://jirametrics.org/troubleshooting/). ## I'm getting an error about a status not being found and was directed here `Warning: The history for issue SP-51 references the status ("Refinement":10012) which can't be found, most likely because it was deleted from Jira after this issue passed through it...` **What's going on.** Jira's status API only ever returns statuses that still exist. If a status was deleted after some issues had already moved through it, the issue history still references it, but Jira can no longer tell us which category it belonged to. So any status you retire mid-flight goes invisible and has to be sorted out by hand. (You would think Jira would keep that information around. It does not.) **Do I have to chase down every deleted status?** No. The warning only fires for the deleted statuses that actually affect this export, so you'll only ever be asked about the ones that matter. Deal with the ones you're warned about and ignore the rest. **It will usually guess correctly.** For an English-language instance, JiraMetrics guesses the category from the name: anything like "To Do", "New" or "Backlog" is treated as `To Do`; "Done", "Closed" or "Cancelled" as `Done`; everything else as `In Progress`. The warning tells you what it guessed, and if that's right you don't need to do anything. You only need to step in when the guess is wrong, or when the name doesn't make the category obvious. **Working out the right category.** It has to be one of `To Do`, `In Progress`, or `Done`. If the name doesn't settle it, look at what the status connects to: run `jirametrics info SP-51` (or open the issue's history in Jira) and see what the missing status transitions into. If work flows out of it into your in-progress statuses, it sits before work has started, so it's a `To Do`. If work flows into it and then stops, it's a `Done`. Anything in between is `In Progress`. **Setting the mapping.** Status names are rarely unique in Jira (plenty of instances have a dozen statuses all called "To Do"), so a mapping is keyed by `"Name":id` and the id is required. The id is right there in the warning (`"Refinement":10012` means id `10012`), and `jirametrics boards ` will list every current status on a board with its id. * With `standard_project`, pass a `status_category_mappings` hash (note the trailing `s`). You can define one hash and share it across several projects: ```ruby retired_statuses = { 'Refinement:10012' => 'To Do', 'Sign Off:10044' => 'Done' } standard_project name: 'Sample', file_prefix: 'sample', boards: { 44 => :default }, status_category_mappings: retired_statuses ``` * With the full `project` DSL, use the singular `status_category_mapping` method [defined here](https://jirametrics.org/config/project/#status_category_mapping). If you're on a version of JiraMetrics earlier than 2.6, a missing status is a fatal error that stops the app rather than a warning. ## I'm getting an error about being rate limited. It might really be that you've hit the Jira instance too often, and you'll have to wait until it resets. It might also be that your access token was deleted on the server and Jira is just returning a misleading message. We've seen both. ---- # Configuration ## How is "stalled" calculated and how do I change that? "Stalled" indicates that the work cannot proceed because the team has no capacity to work on it. If we had someone available, it would be in progress. By default, we consider an item to be stalled if there is no activity in Jira for 5 days. Activity means almost any entry in the issue's changelog, plus comments and the movement of subtasks. There is no list of fields that count; if Jira recorded it in the history then it resets the clock, including low signal changes like Watchers or Rank. So an item that nobody has genuinely worked on can still look active if something incidental touched it. If you are trying to work out why a particular item isn't showing as stalled, `jirametrics info ISSUE-123` will dump its full history so you can see exactly what reset the clock. There is one exception. A few kinds of history entry are not somebody working on the item at all, and those are listed in `stalled_ignored_fields`. It starts with `RemoteIssueLink`, which is what Jira writes when someone drops a Jira issue macro into a Confluence page: that is somebody referencing your work, not doing it, so it no longer resets the clock. Add to the list if your instance has other fields that are similarly incidental. * You can change the number of days in [settings](https://jirametrics.org/config/project/#settings) with the key `stalled_threshold_days` * You can also designate a particular status so that the work immediately becomes stalled when entering this status. That is also in [settings](https://jirametrics.org/config/project/#settings) with the key `stalled_statuses` * You can change which history entries are ignored entirely with the key `stalled_ignored_fields`, also in [settings](https://jirametrics.org/config/project/#settings) ## How is "blocked" calculated and how do I change that? "Blocked" indicates that the work cannot proceed because of some external blocker. By default, the only thing that automatically triggers "blocked" is the Jira flag. When the flag is enabled on a ticket, it's considered blocked. In our experience, this is by far the most common use of the Flag so we turn that on by default. * If your team uses Flagged for some other purpose then you can change that with the [setting](https://jirametrics.org/config/project/#settings) `flagged_means_blocked`. * You can also designate a particular status so that the work immediately becomes blocked when entering this status. That is also in [settings](https://jirametrics.org/config/project/#settings) with the key `blocked_statuses` * An item can be designated as blocked with the use of a link. We can say that ticket ABC-1 is blocked by ABC-2, and we configure that in [settings](https://jirametrics.org/config/project/#settings) with the key `blocked_link_text` ## How do I customize the CSS for the report? Perhaps you want to change the colours on the report or you want to otherwise change the appearance, we give you the ability to insert your own CSS file that will override ours. Instructions are [over here](https://jirametrics.org/project/file_html/#css). ## I need to create a consolidated report that pulls charts from multiple other reports into one place Check out the [stitcher](https://jirametrics.org/stitcher/) ## JiraMetrics isn't correctly finding the parent issue for issues on my board. Jira has a confusing history of how it has attached parents at different points in the past. For this reason, we try several different ways of identifying the parent. One of the ways that it uses is a specific custom field, which is different in every instance, so we can't automatically determine it. Let's assume that you've identified a ticket `ABC-001` and it has a parent ticket `ABC-002` that is not being linked correctly. You need to find the JSON file for ABC-001 which will likely be found in the directory `{target_path}/{file_prefix}_issues` or something like `target/sample_issues`. Inside that file, you need to search for the parent key, `ABC-002` in this case. You'll find it defined in a custom field. If you then take that custom field and put it in the settings as shown, then parents will start to display properly. ``` settings['customfield_parent_links'] = ['customfield_10019'] ``` ## Why is 85% the default percentile? **The short answer.** It's a reasonable proxy for "most". Most of the work will fall on or below the 85% point **What you actually want from a percentile.** A number you can plan around, and that you can say out loud to a stakeholder without misleading them. "Most work of this type finishes within X days." That means picking a number high enough that "most" is honest, and low enough that it is still stable and still useful. **Why not the median?** The 50th percentile is a coin flip. Half your work takes longer than that, by definition. It is genuinely useful for watching whether your typical case is drifting, but as a commitment it fails half the time, which is not a commitment. **Why not the 95th or 98th?** Out there you are in the long tail, where you have very few data points. The number swings wildly because one unusual item moves it, so it is unstable from one report to the next. It is also so conservative that quoting it tends to produce dates nobody believes. It is worth looking at to understand your worst case, but it is a poor planning number. 85% is high enough that "most" is a fair description, low enough to be reasonably stable, and widely enough used in the flow metrics community that other people know what you mean when you say it. That last part matters more than it sounds: a shared convention is worth something even when a neighbouring number would have done just as well. **It is a starting point, not a rule.** If your context calls for something else, change it. See [percentiles](https://jirametrics.org/charts/#choosing-which-percentile-lines-to-draw-with-percentiles) for how, including asking for several at once so you can see the median, the planning number and the worst case side by side. **One caveat about small data sets.** A percentile is only as trustworthy as the number of items behind it. If you have twenty completed items, the 85th percentile is essentially the 17th one, and a single strange item moves it noticeably. This is not a reason to avoid percentiles; it is a reason to be careful about how confidently you quote them when the chart is sparse. ---- # Data issues ## On a team-managed kanban board, why does the data show more items in progress than I see on the board? In a team-managed kanban project, Jira allows items to be in an in-progress status while still sitting in the backlog - they are not visible on the board until you explicitly drag them across. Unfortunately, Jira does not record this _"moved to board"_ action distinctly in the issue changelog. Both dragging an item onto the board and simply reordering items within the backlog produce an identical `Rank` changelog entry, so there is no way to tell them apart from the issue data alone. This means JiraMetrics cannot distinguish between _"in-progress and on the board"_ and _"in-progress but still in the backlog."_ Items in the latter state will be counted as in-progress in the metrics even though they are not visible on the board. The only workaround available today is to use a separate status for backlog items - one that is not mapped to any board column - so that items only enter an in-progress status when they are genuinely being worked on. This is good practice regardless, as it keeps your flow data accurate. ---- # Jira instance types ## Some features, like proper cache invalidation, are marked as Cloud only. Why is that? There are three main reasons. 1. We no longer have access to an instance of Jira Data Center and have to rely on people reporting bugs to try things in their environment. This is very time consuming for everyone. 2. Jira Data Center has already had it's end of life announced (March 2029) and Atlassian themselves are putting very little attention here. 3. The API's between Cloud and Data Center are already starting to deviate and so implementing the same feature for both, sometimes requires completely different implementations. The bottom line is that it makes no sense for us to continue adding functionality here, when the whole platform is going away. Particularly when that feature development is more time consuming for us. If you're using Data Center today and have budget to fund development of features for Data Center then we're happy to entertain that; see our [support options](/support). We're unlikely to be adding to the Data Center support otherwise, however. ================================================================================ # MCP Server (AI Integration) Source: https://jirametrics.org/mcp/ ================================================================================ The JiraMetrics MCP server connects Claude directly to your Jira data. Ask questions in plain English and get answers that would otherwise require custom queries or reports: - "Show me all aging work older than 60 days in the Mobile team project" - "What's been sitting in Review the longest?" - "How many items were cancelled last month?" - "What backlog items have ever been flagged as blocked?" # What is MCP? [MCP (Model Context Protocol)](https://modelcontextprotocol.io) is a standard that allows AI assistants such as Claude to directly query data from tools like JiraMetrics. Instead of copying and pasting data into a conversation, the AI can query your Jira data directly and reason about it in context. # Setup For the latest MCP features, install the prerelease: `gem install jirametrics --pre` The MCP server writes its own log to `jirametrics-mcp.log`, kept separate from the `jirametrics.log` that `download` and `export` use. That way, starting the server (which your AI tool does automatically when you launch it from a project directory) never overwrites a `jirametrics.log` you're in the middle of debugging from. ## Claude Code Claude Code supports per-project MCP configuration via a `.mcp.json` file in your project directory. This is the recommended approach because each directory can point to a different Jira instance, keeping client data fully isolated. Create a `.mcp.json` file in the same directory as your `config.rb`: ```json { "mcpServers": { "jirametrics": { "type": "stdio", "command": "jirametrics", "args": ["mcp"] } } } ``` This simplest form works whenever the MCP host can find `jirametrics` on its `PATH`, which is usually the case when you start Claude Code from a terminal where `jirametrics` already runs. If Claude Code reports that it can't start the server, the host was almost certainly launched without your Ruby version manager's environment, so a bare `jirametrics` isn't on its `PATH`. This is the single most common setup problem, and it is not specific to Claude Code: any MCP host launched outside your normal shell hits it. The fix is always the same, give it the absolute path. Run `which jirametrics` (or `where jirametrics` on Windows) in your terminal and use whatever it prints as the `command` instead: ```json { "mcpServers": { "jirametrics": { "type": "stdio", "command": "/Users/you/.rbenv/shims/jirametrics", "args": ["mcp"] } } } ``` Typical locations that `which jirametrics` reports: | Ruby setup | Path | |:-----------|:-----| | rbenv | `~/.rbenv/shims/jirametrics` | | asdf | `~/.asdf/shims/jirametrics` | | RVM | `~/.rvm/gems//bin/jirametrics` | | No version manager | `/usr/local/bin/jirametrics` or similar | | RubyInstaller (Windows) | `C:/Ruby/bin/jirametrics` | On Windows, use forward slashes in the paths (`C:/Ruby/bin/...`). They work in both JSON and TOML config files and save you from having to escape every backslash as `\\`. For rbenv and asdf the shim re-executes under the correct Ruby on its own, so the absolute path is all you need. RVM is the exception: its gem executables expect RVM's shell environment to be loaded first, so for RVM use a shell that sources it: ```json { "mcpServers": { "jirametrics": { "type": "stdio", "command": "/bin/bash", "args": ["-c", "source ~/.rvm/scripts/rvm && jirametrics mcp"] } } } ``` When you start Claude Code from that directory, the MCP server starts automatically and the `jirametrics` tools become available in your conversation. ### Data isolation between instances Because `.mcp.json` is per-directory, each directory has its own configuration pointing to its own `config.rb` and its own downloaded data. Starting Claude Code from directory A gives you only that instance's data; starting from directory B gives you only that instance's data. There is no cross-contamination. Add `.mcp.json` to your `.gitignore` if the file contains paths specific to your machine. ## AI assistants that require a single-word command Some AI assistants (such as [apfel](https://github.com/Arthur-Ficial/apfel)) invoke MCP servers by executing a single command with no arguments. They cannot call `jirametrics mcp` because the space makes it look like a path to a binary named `jirametrics mcp` rather than a command with a subcommand. For these cases, JiraMetrics ships a second executable, `jirametrics-mcp`, which is equivalent to running `jirametrics mcp`. Pass any options you would normally pass to `jirametrics mcp` directly to `jirametrics-mcp`: ``` jirametrics-mcp --config /full/path/to/your/config.rb ``` With apfel, for example: ``` apfel --mcp $(which jirametrics-mcp) "what boards do we have?" ``` Apfel resolves the command as a file path rather than searching your PATH, so you need to pass the full path. `$(which jirametrics-mcp)` expands to the correct path at the time you run the command. ## Claude Desktop Claude Desktop loads all configured MCP servers at startup and runs them for the entire session. This works well if you only have one Jira instance to work with. Add a `jirametrics` entry to your `claude_desktop_config.json` (on macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`): ```json { "mcpServers": { "jirametrics": { "command": "/bin/bash", "args": [ "-c", "source ~/.rvm/scripts/rvm && jirametrics mcp --config /full/path/to/your/config.rb" ] } } } ``` Restart Claude Desktop after making changes to this file. If you have multiple JiraMetrics instances with confidentiality requirements between them, Claude Desktop is not recommended as all configured servers run simultaneously in the same session. ## Mistral Vibe CLI [Vibe](https://docs.mistral.ai/vibe/code/cli/mcp-servers) configures MCP servers in a `config.toml`. It runs the server over stdio, so point `command` at the `jirametrics-mcp` executable and pass your config with `--config`: ```toml [[mcp_servers]] name = "jirametrics" transport = "stdio" command = "C:/Ruby/bin/jirametrics-mcp" args = ["--config", "C:/development/myreports/config.rb"] ``` If Vibe reports something like `MCP stdio discovery failed`, it's the PATH problem described under [Claude Code](#claude-code): Vibe launched without your Ruby environment and couldn't find a bare `jirametrics-mcp`, so it never got far enough to log anything useful. Give it the absolute path to the executable (`where jirametrics-mcp`, or `which jirametrics-mcp` on macOS/Linux) as shown above. # Available tools ## list_projects Returns all available projects with their issue count and data end date, plus any aggregate groups defined in your config. Claude will call this automatically when a question is ambiguous about which project it applies to, allowing it to ask you to clarify before running a broader query. This tool takes no parameters. Aggregate groups appear at the bottom of the output and can be used as the `project` parameter in any tool - they expand automatically to cover all constituent projects. For example, if your config defines a `forest` aggregate over Pine, Maple, Willow, Birch, and Cedar, you can ask "what's aging in forest?" and it will query all five. ## aging_work Returns all issues that have been started but not yet completed (work in progress), sorted from oldest to newest. Age is the number of days since the issue was started, calculated relative to the end of the downloaded data range - consistent with how the aging charts in the HTML report calculate age. Flow efficiency (FE) is also returned for each issue: the percentage of elapsed time the issue was actively being worked on. | Parameter | Type | Description | |:----------|:-----|:------------| | `min_age_days` | integer | Only return issues at least this many days old. Omit to return all. | | `project` | string | Only return issues from this project name. Omit to return all projects. | | `current_status` | string | Only return issues currently in this status (e.g. `"Review"`, `"In Progress"`). | | `current_column` | string | Only return issues whose current status maps to this board column (e.g. `"In Progress"`). | | `history_field` | string | Only return issues where this field ever had the value specified by `history_value` (e.g. `"priority"`). Must be used together with `history_value`. | | `history_value` | string | The value to look for in the change history of `history_field` (e.g. `"Highest"`). Must be used together with `history_field`. | | `ever_blocked` | boolean | Only return issues that were ever blocked. Blocked includes flagged items, issues in blocked statuses, and blocking issue links. | | `ever_stalled` | boolean | Only return issues that were ever stalled. Stalled means the issue sat inactive for longer than the stalled threshold, or entered a stalled status. | | `currently_blocked` | boolean | Only return issues that are blocked right now (as of the data end date). | | `currently_stalled` | boolean | Only return issues that are stalled right now (as of the data end date). | ### Examples Ask Claude naturally - it will choose the right parameters: - "Show me all aging work" - returns everything - "Show aging work older than 90 days" - uses `min_age_days: 90` - "What's aging in the Mobile project?" - uses `project: "Mobile"` - "Show aging work older than 60 days in the Mobile project" - uses both filters - "Show aging work that was ever priority Highest" - uses `history_field: "priority"`, `history_value: "Highest"` - "What aging work has been blocked?" - uses `ever_blocked: true` - "What aging work is currently blocked?" - uses `currently_blocked: true` - "Show work in progress that has stalled" - uses `ever_stalled: true` - "What's stalled right now?" - uses `currently_stalled: true` - "Show me all aging work currently in Review" - uses `current_status: "Review"` - "Show me aging work in the In Progress column" - uses `current_column: "In Progress"` ## completed_work Returns issues that have been completed, sorted most recently completed first. Includes cycle time (days from start to completion), flow efficiency (FE - the percentage of cycle time spent actively working on the issue), and the status and resolution at the time of completion. | Parameter | Type | Description | |:----------|:-----|:------------| | `days_back` | integer | Only return issues completed within this many days of the data end date. Omit to return all. | | `project` | string | Only return issues from this project name. Omit to return all projects. | | `completed_status` | string | Only return issues whose status at completion matches this value (e.g. `"Done"`, `"Cancelled"`). | | `completed_resolution` | string | Only return issues whose resolution at completion matches this value (e.g. `"Won't Do"`). | | `history_field` | string | Only return issues where this field ever had the value specified by `history_value`. Must be used together with `history_value`. | | `history_value` | string | The value to look for in the change history of `history_field`. Must be used together with `history_field`. | | `ever_blocked` | boolean | Only return issues that were ever blocked. | | `ever_stalled` | boolean | Only return issues that were ever stalled. | | `currently_blocked` | boolean | Only return issues that were blocked at the time of completion. | | `currently_stalled` | boolean | Only return issues that were stalled at the time of completion. | ### Examples - "What work was completed in the last 30 days?" - uses `days_back: 30` - "How many items were cancelled?" - uses `completed_status: "Cancelled"` - "Show completed work that was never started (no cycle time)" - cycle time shown as "unknown" - "What completed work was ever flagged as highest priority?" - uses `history_field: "priority"`, `history_value: "Highest"` - "Show completed items that were blocked at some point" - uses `ever_blocked: true` ## not_yet_started Returns issues that have not yet been started (backlog items), sorted by creation date oldest first. | Parameter | Type | Description | |:----------|:-----|:------------| | `project` | string | Only return issues from this project name. Omit to return all projects. | | `current_status` | string | Only return issues currently in this status (e.g. `"To Do"`, `"Backlog"`). | | `current_column` | string | Only return issues whose current status maps to this board column (e.g. `"Ready"`). | | `history_field` | string | Only return issues where this field ever had the value specified by `history_value`. Must be used together with `history_value`. | | `history_value` | string | The value to look for in the change history of `history_field`. Must be used together with `history_field`. | | `ever_blocked` | boolean | Only return issues that were ever blocked. | | `ever_stalled` | boolean | Only return issues that were ever stalled. | | `currently_blocked` | boolean | Only return issues that are blocked right now (as of the data end date). | | `currently_stalled` | boolean | Only return issues that are stalled right now (as of the data end date). | ### Examples - "What's in the backlog?" - returns all unstarted issues - "What's been sitting in the backlog the longest?" - oldest items appear first - "Show unstarted work in the Mobile project" - uses `project: "Mobile"` - "What backlog items have ever been flagged?" - uses `history_field: "Flagged"`, `history_value: "Impediment"` - "Show backlog items in the To Do status" - uses `current_status: "To Do"` ## status_time_analysis Aggregates the time issues spend in each status or column, ranked by average days per issue. Useful for identifying bottlenecks and understanding where flow slows down. Time in status is counted from issue creation through to completion (or data end date for in-progress issues). Because the answer differs significantly depending on which issues are included, Claude will ask you to clarify the `issue_state` before running this tool if it isn't clear from context. | Parameter | Type | Description | |:----------|:-----|:------------| | `project` | string | Only include issues from this project. Omit to include all projects. | | `issue_state` | string | Which issues to include: `"aging"` (in progress), `"completed"`, `"not_started"` (backlog), or `"all"` (default). | | `group_by` | string | Group results by `"status"` (default) or `"column"`. Column-level grouping aggregates multiple statuses that map to the same board column. | ### Examples - "In what status do issues spend the most time?" - no parameters needed - "In what column do issues spend the most time?" - uses `group_by: "column"` - "Where is work getting stuck for completed issues?" - uses `issue_state: "completed"` - "What's the bottleneck in the Mobile project?" - uses `project: "Mobile"` - "Where does aging work tend to pile up?" - uses `issue_state: "aging"` - "Which column is the biggest bottleneck?" - uses `group_by: "column"`, `issue_state: "completed"` ================================================================================ # Probabilistic Forecasting Source: https://jirametrics.org/forecasting/ ================================================================================ One of the most common reasons we want to look at metrics is so that we can more accurately answer the question _"when will we be done?"_ With those metrics, we can create a [probabilistic forecast](https://blog.mikebowler.ca/2024/06/02/probabilistic-forecasting/) that answers that question based on historical data. ## Single item forecasting JiraMetrics can generate a single-item forecast for items that are already in progress. You can see this on the [Aging Work Table](https://jirametrics.org/charts/#aging_work_table), where the forecast column will show you how many days are likely remaining. If a forecast can't be generated then an error will be shown with the details of why that is. ## Monte Carlo forecasting Although JiraMetrics doesn't create a monte carlo forecast for you, it can easily extract the data you need to do a forecast in tools like [Actionable Agile](https://actionableagile.com) (commercial) or the [Focused Objective throughput forecaster](https://www.focusedobjective.com/pages/free-spreadsheets-and-tools) (free). If you haven't used JiraMetrics before then you'll likely want to start with the [QuickStart](https://jirametrics.org/quickstart/) and then you can come back here for the configuration to help with probabilistic forecasting. To use the **Focused Objective** spreadsheet, you'll need only the throughput data across multiple weeks and you can easily get that by looking at the [`throughput_chart`](https://jirametrics.org/charts/#throughput_chart). Using **Actionable Agile** is a little trickier as it requires more data in a specific format but even this isn't hard. The configuration below will generate exactly what you need. Note that only the part within `columns` is specific to this output. The rest is only provided for context. ```ruby Exporter.configure do timezone_offset '-05:00' target_path 'target/' jira_config 'jira_config_improvingflow.json' project name: 'myproject' do file_prefix 'myproject' download do rolling_date_count 90 end board id: 1 do cycletime do start_at first_time_in_status_category('In Progress') stop_at still_in_status_category('Done') end end discard_changes_before status_becomes: :backlog # 'To Do' file do file_suffix '.csv' columns do write_headers true string 'ID', key string 'link', url string 'title', summary column_entry_times end end end end ``` ================================================================================ # Quality Report Source: https://jirametrics.org/data_quality/ ================================================================================ The charts we look at will only be as good as the data that went into them, even though our own [cognitive biases](https://blog.mikebowler.ca/2024/11/10/cognitive-bias) will lead us to believe otherwise. It's critical that we understand how good the data is, before we use it to make decisions. At the top of every report, we have a "Data Quality" section and in that, we list those things that we think are important to note. The items highlighted there are not necessarily bad data. Some are genuine contradictions worth fixing in Jira, such as an item finishing before it started. But many are simply places where the way your team actually works bumps up against the simplified start-to-finish model this tool uses to compute flow metrics. Those aren't mistakes; we flag them so you can read the charts with the right context, not because anyone did anything wrong. What do we check for? 1. Items that were moved "back to the backlog" after being started. This is a poor practice and yet almost every team does it. 2. Items for which we know when they completed but can't tell when they started. This usually means items moving directly from ToDo to Done. 3. Items that continued to have status changes after they were identified as having completed. Likely what we're considering 'done' isn't really done. 4. Items that moved backwards on the board. Almost always a poor practice. 5. Items that are in progress but for whatever reason, are not visible on the board. 6. Items that were created directly into a status that isn't part of the backlog, rather than starting there. Usually this means the item was created from a column on the board. 7. Items that are considered 'done' before they even started. 8. Items that are considered to not have started, yet some of their subtasks have started. Almost certainly a mistake. 9. Items that have been declared as done but which still have active subtasks. 10. Items that show up on multiple boards and that are likely being included in multiple sets of metrics. 11. Items that are marked as blocked by another issue that has already been completed, so it can no longer really be blocking. ================================================================================ # Security Concerns Source: https://jirametrics.org/security/ ================================================================================ In many companies, there are valid concerns about letting people directly access the data in our Jira instance. This page should give you enough information to understand the potential risks so you can decide if allowing this in your environment is the right answer. TL;DR: No additional risk is introduced by installing JiraMetrics. Any risk present, was already there. ## Where does the information come from and where is it shared? JiraMetrics retrieves information through the publicly available API on your Jira instance. It calls only your Jira instance and no other servers. Information retrieved through this API is stored on the file system of the machine that made the request and is not shared anywhere else. ## We have confidential information in our Jira instance. How do we restrict access to only certain projects? JiraMetrics has the same access level as the person who is running it. If that person can access your confidential projects through a web browser then that same information is available through the API. There is no additional risk here as you had already granted access to this person. Note that this information is available through the API whether or not you use JiraMetrics. ## Is there auditing so we can see who is calling the API and what information they have retrieved? I don't know. That would require looking at the Jira logs and only your Jira administrator would have access to that. We call publicly available API's. ## Does JiraMetrics write any information into the Jira instance? No, we read data only. No information is written back to Jira.