{"id":9905,"date":"2018-12-25T00:40:49","date_gmt":"2018-12-25T05:40:49","guid":{"rendered":"https:\/\/qxf2.com\/blog\/?p=9905"},"modified":"2018-12-25T00:40:49","modified_gmt":"2018-12-25T05:40:49","slug":"python-jira-analyze-engineering-metrics","status":"publish","type":"post","link":"https:\/\/qxf2.com\/blog\/python-jira-analyze-engineering-metrics\/","title":{"rendered":"Analyze JIRA data with Python"},"content":{"rendered":"<p>Most of our clients (Agile software teams) use <a href=\"https:\/\/www.atlassian.com\/software\/jira\">Atlassian Jira<\/a> for managing tickets and sprints. Every day, we keep updating the Jira for all tasks that are being worked upon.\u00a0We realized that Jira has huge project\/team data logs but Jira reports were not that helpful in capturing work habits of teams. Hence, <a href=\"https:\/\/www.qxf2.com\/?utm_source=jira1&#038;utm_medium=click&#038;utm_campaign=From%20blog\">Qxf2<\/a> has ended up developing an &#8216;Engineering Benchmarks&#8217; web application using Python and Flask to generate different engineering metrics based on Jira data.\u00a0These metrics help our engineering teams in identifying internal problems and track their progress.<\/p>\n<p>&nbsp;<\/p>\n<h3>Why this post?<\/h3>\n<p>We got tired of hearing developers say &#8220;QA is the bottleneck&#8221;. As experienced QA, we suspected the problem really was twofold:<br \/>\na) poor work habits of developers, E.g.: tickets arriving in a bunch at the end of the sprint.<br \/>\nb) wait times seem longer than they are, E.g.: developers rarely realize how long they spend on a ticket!<\/p>\n<p>But there was no easy way to prove our hypotheses. JIRA reports did not help and asking people to track their time seemed to be a very costly and error-prone option. So, out of sheer frustration, we ended up building Python scripts to measure two things:<br \/>\n1. How long a ticket spent in different states?<br \/>\n2. Timeline in which JIRA tickets arrived at a particular state (immediately exposes bursty behavior!)<\/p>\n<p>Since then, we have used this code to figure out a lot more work habits like poorly commented tickets, how often pairs of people collaborate over JIRA, etc. Internally, we call this project &#8216;engineering benchmarks&#8217;. We thought of sharing the details of our engineering benchmarks application development and few engineering metrics as well to help other engineering teams who like to create their own Jira metrics with graphs.<\/p>\n<p>&nbsp;<\/p>\n<h3>What is in this post?<\/h3>\n<p>In this post, I will share the details of two Python modules Qxf2 wrote and feel would be useful to all the teams starting out on a similar exercise. I will be going through the details about one of the important back-end modules called ConnectJira\u00a0which contains wrappers for all the Jira related API calls and the JiraDatastore module which saves ticket information. <\/p>\n<p>&nbsp;<\/p>\n<h3 style=\"text-align: left;\">The ConnectJira\u00a0module<\/h3>\n<p>This module provides wrappers around Jira&#8217;s REST API. We are limiting the code snippets to list only some of the most frequently used calls in our analysis. You can see the entire code for ConnectJira.py in <a href=\"https:\/\/gist.github.com\/qxf2\/53958d4dd797988c1ad4f21875dd66cb\">this GitHub gist<\/a>. For your convenience, we will help you get setup and explain a few of the methods we ended up using quite a lot in our analysis.<\/p>\n<h4>a. Install jira-python through pip<\/h4>\n<p>You can install the <code>jira<\/code> Python module by running the below command on your command prompt:<\/p>\n<pre lang=\"python\"> $ pip install jira<\/pre>\n<p>To test if your install worked, pull up a Python interpreter and try the below lines: <\/p>\n<pre lang=\"python\">from jira import JIRA\r\nfrom jira import JIRAError\r\n<\/pre>\n<p>If your install worked, you should not see any import error.<\/p>\n<h4>b. Interact with Jira client using an instance of this module<\/h4>\n<p>You can create a Jira client easily:<\/p>\n<pre lang=\"python\">connect_jira_obj = ConnectJira(JIRA_URL, USERNAME, PASSWORD, PROJECT)<\/pre>\n<p>You can access any connection error details by using the <code>connect_jira_obj.error<\/code>.<\/p>\n<h4>c. Get Jira\u00a0ticket list<\/h4>\n<p>Fetching the result of a JQL is one of the most common operations you will perform: <\/p>\n<pre lang=\"python\">jql = \"project=%s\"%project\r\nconnect_jira_obj.execute_query(jql)\r\nreturn {'ticket_list': ticket_list,'error': error} \r\n<\/pre>\n<p>Parameters: jql, Return type: dict<br \/>\nThe returned Jira ticket list data view will look something like this:<\/p>\n<p><a href=\"https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2018\/10\/ExecuteQuery-1.png\" data-rel=\"lightbox-image-0\" data-rl_title=\"\" data-rl_caption=\"\" title=\"\"><img loading=\"lazy\" decoding=\"async\" class=\"alignleft wp-image-9922 size-full\" src=\"https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2018\/10\/ExecuteQuery-1.png\" alt=\"\" width=\"705\" height=\"95\" srcset=\"https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2018\/10\/ExecuteQuery-1.png 705w, https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2018\/10\/ExecuteQuery-1-300x40.png 300w\" sizes=\"auto, (max-width: 705px) 100vw, 705px\" \/><\/a><\/p>\n<h4>d. Get jira ticket in json with changelog expanded and selected <\/h4>\n<p>If you ever want to dig into the history of a ticket (e.g.: how often did it enter the &#8216;in testing&#8217; state), you will need to become friends with a ticket&#8217;s changelog. Here is how you can fetch a ticket with its complete changelog.<\/p>\n<pre lang=\"python\">connect_jira_obj.get_ticket_in_json(ticket)\r\nreturn {'ticket': ticket_expanded, 'error': error}\r\n<\/pre>\n<p>Parameters: Ticket key, Return type: dict<br \/>\nThe returned json format ticket data view using (thanks <a href=\"https:\/\/jsonformatter.curiousconcept.com\/\">JSON formatter!<\/a>) will look like this:<\/p>\n<p><a href=\"https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2018\/10\/jsonticket.png\" data-rel=\"lightbox-image-1\" data-rl_title=\"\" data-rl_caption=\"\" title=\"\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-9924\" src=\"https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2018\/10\/jsonticket.png\" alt=\"\" width=\"911\" height=\"558\" srcset=\"https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2018\/10\/jsonticket.png 911w, https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2018\/10\/jsonticket-300x184.png 300w, https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2018\/10\/jsonticket-768x470.png 768w\" sizes=\"auto, (max-width: 911px) 100vw, 911px\" \/><\/a><\/p>\n<h4>e.Get the Jira workflow statuses<\/h4>\n<p>Sometimes, you will want to know all the statuses that are present in a Jira instance. You can do that like this:<\/p>\n<pre lang=\"python\">connect_jira_obj.get_statuses_from_jira()\r\nreturn {'statuses':statuses, 'error':error}\r\n<\/pre>\n<p>No Parameters, Return type:dict<br \/>\nThe returned data looks something like this:<\/p>\n<pre lang=\"python\">{'statuses': [u'open', u'reopened', u'closed', u'to do', u'in dev', u'code review', u'ready for test', u'in test', u'peer review', u'reopen', u'integration test'], 'error': None}\r\n<\/pre>\n<h4>f. Get the list of Jira boards that are associated with the project<\/h4>\n<p>Fetching sprint boards to analyze is useful too:<\/p>\n<pre lang=\"python\">connect_jira_obj.get_jira_project_boards(jira_project)\r\njira_project_boards = [{'id':board.id,'name':board.name}] \r\nreturn {'jira_project_boards':jira_project_boards,'error':error}\r\n<\/pre>\n<p>Parameter: jira project name or Id , Return type:dict<br \/>\nThe returned list of jira boards looks something like this:<\/p>\n<pre lang=\"python\">[JIRA Board: name=u'EN board', id=1, JIRA Board: name=u'QTR board', id=3, JIRA Board: name=u'TDP board', id=2]<\/pre>\n<h4>g. Get the list of Jira sprint ids for the given Jira boards<\/h4>\n<p>Sometimes we want to analyze metrics on a sprint by sprint basis:<\/p>\n<pre lang=\"python\">connect_jira_obj.get_jira_project_sprints(jira_project_boards)\r\nreturn {'jira_project_sprints':jira_project_sprints,'error':error}\r\n<\/pre>\n<p>Parameter: list of the jira project boards, Return type:dict<br \/>\nYou will see the returned list of Jira sprint ids<\/p>\n<p><a href=\"https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2018\/10\/sprint_list.png\" data-rel=\"lightbox-image-2\" data-rl_title=\"\" data-rl_caption=\"\" title=\"\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-9930\" src=\"https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2018\/10\/sprint_list.png\" alt=\"\" width=\"1352\" height=\"135\" srcset=\"https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2018\/10\/sprint_list.png 1352w, https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2018\/10\/sprint_list-300x30.png 300w, https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2018\/10\/sprint_list-768x77.png 768w, https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2018\/10\/sprint_list-1024x102.png 1024w\" sizes=\"auto, (max-width: 1352px) 100vw, 1352px\" \/><\/a><\/p>\n<h4>h. Get ticket list for the given sprint id<\/h4>\n<pre lang=\"python\">connect_jira_obj.get_sprint_ticket_list(sprint_id)\r\nreturn {'sprint_ticket_list':sprint_ticket_list,'error':error}\r\n<\/pre>\n<p>Parameter: sprint id, Return type:dict<\/p>\n<h4>i. Get sprint details for the given sprint id<\/h4>\n<pre lang=\"python\">connect_jira_obj.get_sprint_details(sprint_id)\r\nreturn {'sprint_details':sprint_details, 'error':error}\r\n<\/pre>\n<p>Parameter: sprint id, Return type:dict<br \/>\nReturned sprint_details data view<\/p>\n<pre lang=\"python\">{'sprint_details': JIRA Sprint: name=u'EN Sprint 1', id=1, 'error': None}<\/pre>\n<p>&nbsp;<\/p>\n<h3>Storing Jira Tickets<\/h3>\n<p>We found it is useful to fetch and save individual tickets as pickled files. That way, we only needed to update our datastore with recently changed tickets rather than fetching all the tickets we were analyzing over a timeframe. All Jira tickets are stored using Python pickle. Here are some of the useful methods we ended up writing and a short write up of how updating the datastore works.<\/p>\n<p><strong>Note:<\/strong> You can see the entire code for JiraDatastore.py in this <a href=\"https:\/\/gist.github.com\/qxf2\/3396f4c633c2e883c287c04da541b7cd\">GitHub gist<\/a>.<\/p>\n<p>1. Update datastore will be run based on the last updated date stored in pickle<\/p>\n<pre lang=\"python\">jql = \"project = '%s' AND updatedDate &gt;= '%s' ORDER BY updated DESC\" % (self.project, last_updated_date)\r\njql_result = self.connect_jira_obj.execute_query(jql)\r\nticket_list = jql_result['ticket_list']\r\nself.store_ticket_list(ticket_list)\r\n<\/pre>\n<p>2. If datastore does not exist already for the given Jira client and project, we run complete datastore for the given project<\/p>\n<pre lang=\"python\">jql = \"project='%s' ORDER BY updated DESC\" % (self.project)\r\njql_result = self.connect_jira_obj.execute_query(jql)\r\nticket_list = jql_result['ticket_list']\r\nself.store_ticket_list(ticket_list)\r\n<\/pre>\n<p>3. We make sure to run update_datastore in the backend code before fetching tickets from our pickle db to generate metrics for the up-to-date Jira data<\/p>\n<pre lang=\"python\">self.datastore_obj.update_datastore()<\/pre>\n<hr \/>\n<p>You can have a look at thorough details of some interesting engineering metrics that we have created in the following blogs. Feel free to write to us if you like to create engineering team Jira metrics.<\/p>\n<p><strong>NOTE:<\/strong> While Qxf2 has the habit of <a href=\"https:\/\/github.com\/qxf2\">open sourcing many of our R&#038;D projects<\/a>, we will not be open sourcing this code in the near future. <\/p>\n<hr\/>\n","protected":false},"excerpt":{"rendered":"<p>Most of our clients (Agile software teams) use Atlassian Jira for managing tickets and sprints. Every day, we keep updating the Jira for all tasks that are being worked upon.\u00a0We realized that Jira has huge project\/team data logs but Jira reports were not that helpful in capturing work habits of teams. Hence, Qxf2 has ended up developing an &#8216;Engineering Benchmarks&#8217; [&hellip;]<\/p>\n","protected":false},"author":6,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[172,171,18],"tags":[],"class_list":["post-9905","post","type-post","status-publish","format-standard","hentry","category-flask","category-jira","category-python"],"_links":{"self":[{"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/posts\/9905","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/users\/6"}],"replies":[{"embeddable":true,"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/comments?post=9905"}],"version-history":[{"count":100,"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/posts\/9905\/revisions"}],"predecessor-version":[{"id":15569,"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/posts\/9905\/revisions\/15569"}],"wp:attachment":[{"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/media?parent=9905"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/categories?post=9905"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/tags?post=9905"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}