This article describes how you can build a Sprint Burndown chart for your team
This custom metric requires Custom Selection. Inside this Custom Selection we see several stages of calculation resulting in the “simple” chart. Let’s go through these stages
Creating a Metric
See Custom Metric development for details
Creating a Custom Selection
See Custom Metric development to see how to get to a Custom Selection mode.
Further will be the explanation of the code you should put in the “PerfQL” field
Field Number Mapping in Ticket History
field_number_mapping as ( select 'status' as field, (select field from TicketHistory where status is not null and length(status) > 0 limit 1) as num union select 'story_points' as field, (select field from TicketHistory where story_points is not null limit 1) as num union select 'sprint' as field, (select field from TicketHistory where sprint is not null and length(sprint) > 0 limit 1) as num ),
This comes from a format TicketHistory is stored in. One of the columns describes the number of a field of a Ticket that was modified in that TicketHistory entry. Since these numbers may vary from project to project and from Jira Instance to Jira Instance, we need to find out what numbers correspond to the fields we are going to be interested in: Story Points, Sprint, Status
Result Example:
field | num |
---|---|
story_points | 7 |
sprint | 1 |
status | 0 |
Then we save these numbers to “variables” (results of common table expressions) just for convenience:
status_field as ( select num from field_number_mapping where field = 'status' limit 1 ), sprint_field as ( select num from field_number_mapping where field = 'sprint' limit 1 ), story_points_field as ( select num from field_number_mapping where field = 'story_points' limit 1 ),
Define what are the closed statuses
This is where you customise thew metric to fit your project needs
closed_statuses as ( select a from ( values ('Closed'), ('Verified') ) s(a) ),
This step will be eliminated once the support of TTS Settings in Custom Metrics is introduced, which is pretty soon, but for now here you can define which statuses you count as “closed”
Get the Sprint you want to count the Burndown Chart for
This is where you customise thew metric to fit your project needs
last_sprint as ( -- get the last started sprint select * from Sprint where start_date is not null and state = 'ACTIVE' order by start_date desc limit 1),
Then just for convenience save the search expression for this sprint name
last_sprint_like as ( select concat('%', (select id from last_sprint), '%') ),
Get Related Tickets
Get the Tickets that are in any way related to the sprint we chose
affected_tickets as ( -- get all tickets somehow related to this sprint select distinct(t.*) from Ticket t join TicketHistory th on th.workitem_id = t.id where th.sprint like concat('%', (select id from last_sprint),'%') -- Ticket was ever added to sprint or (select id from last_sprint) = any(t.sprints) /* Ticket is in sprint right now (including the case when it was added to sprint on ticket creation, which is not usually mentioned in Jira history)*/ ),
Get All History entries related to this sprint
affected_history as ( -- get all history items related to the tickets related to sprint select th.workitem_id, th.start, th.field, th.status, th.sprint, th.story_points, th.start = t.created as on_creation from affected_tickets t join TicketHistory th on th.workitem_id = t.id and th.field in (select num from field_number_mapping) union select t.id as workitem_id, t.created as start, (table status_field) as field, t.status, null as sprint, null as story_points, true as on_creation from affected_tickets t left join TicketHistory th on th.workitem_id = t.id and th.field = (table status_field) where th.start is null -- ^^ to substitute lack of history on ticket creation for status union select t.id as workitem_id, t.created as start, (table sprint_field) as field, null as status, array_to_string(t.sprints, ', ') as sprint, null as story_points, true as on_creation from affected_tickets t left join TicketHistory th on th.workitem_id = t.id and th.field = (table sprint_field) where th.start is null -- ^^ to substitute lack of history on ticket creation for sprint union select t.id as workitem_id, t.created as start, (table story_points_field) as field, null as status, null as sprint, t.story_points, true as on_creation from affected_tickets t left join TicketHistory th on th.workitem_id = t.id and th.field = (table story_points_field) where th.start is null -- ^^ to substitute lack of history on ticket creation for story_points ),
Get Date Changes in Affected History
Purely for convenience reasons
changes_dates as ( select distinct workitem_id, start, on_creation from affected_history -- ^^ will need this further for grouping history entries by time (several changes may have been done at the same time but stored as different history items) ),
Bring history entries to the format “was → became”
became_extraction as ( -- moving to format was -> became for 3 fields in focus. This is the 'became' part select *, (case when exists(select ah.sprint from affected_history ah where ah.start = cd.start and ah.workitem_id = cd.workitem_id and ah.field = (table sprint_field)) then coalesce((select ah.sprint from affected_history ah where ah.start = cd.start and ah.workitem_id = cd.workitem_id and ah.field = (table sprint_field) limit 1), 'No Sprint') else null end) as became_sprint, -- if there are no history items where sprint was changed at this moment, there'll be null, otherwise the value of that 'same moment' sprint change (case when exists (select ah.status from affected_history ah where ah.start = cd.start and ah.workitem_id = cd.workitem_id and ah.field = (table status_field)) then coalesce ((select ah.status from affected_history ah where ah.field = (table status_field) and ah.start = cd.start and ah.workitem_id = cd.workitem_id limit 1), 'No Status') else null end) -- if there are no history items where status was changed at this moment, there'll be null, otherwise the value of that 'same moment' status change as became_status, (case when exists(select ah.story_points::text from affected_history ah where ah.start = cd.start and ah.workitem_id = cd.workitem_id and field = (table story_points_field)) then coalesce((select ah.story_points::text from affected_history ah where ah.start = cd.start and ah.workitem_id = cd.workitem_id and field = (table story_points_field)limit 1), 'No SPs') else null end) -- if there are no history items where story_points was changed at this moment, there'll be null, otherwise the value of that 'same moment' story_points change as became_story_points from changes_dates cd ), was_extraction as ( -- this is the 'was' part for corresponding 'became' entries select *, (select coalesce(ah.sprint, 'No Sprint') from affected_history ah where field = (table sprint_field) and ah.start < cd.start and ah.workitem_id = cd.workitem_id order by ah.start desc limit 1) as was_sprint, (select coalesce(ah.status, 'No Status') from affected_history ah where field = (table status_field) and ah.start < cd.start and ah.workitem_id = cd.workitem_id order by ah.start desc limit 1) as was_status, (select coalesce(ah.story_points::text, 'No SPs') from affected_history ah where field = (table story_points_field) and ah.start < cd.start and ah.workitem_id = cd.workitem_id order by ah.start desc limit 1) as was_story_points from changes_dates cd where on_creation = false -- because if the change was done after creation, jira creates 2 history items - one for the creation moment, one for change (donno why) ), history_extraction as ( -- here we combine 'was' and 'became' using join to get all the fields. select from this table to see results select be.workitem_id, be.start, be.on_creation, coalesce(we.was_sprint, 'No Sprint') as was_sprint, coalesce(be.became_sprint, we.was_sprint) as became_sprint, coalesce(we.was_status, 'No Status') as was_status, coalesce(be.became_status, we.was_status) as became_status, coalesce(we.was_story_points, 'No SPs') as was_story_points, coalesce(be.became_story_points, we.was_story_points) as became_story_points from became_extraction be left join was_extraction we on be.workitem_id = we.workitem_id and be.start = we.start ),
Example of the output:
Make sense of “was->became” format related to the Sprint in question
meaningful_history_extraction as ( -- here we apply criterias to fields so that they make more sense to us when making decision on whether add or subtract story points from sprint scope based on what happened with the ticket at that moment select he.workitem_id, he.start, he.on_creation, he.was_sprint like (table last_sprint_like) as was_my_sprint, he.became_sprint like (table last_sprint_like) as became_my_sprint, he.was_status in (table closed_statuses) as was_closed, he.became_status in (table closed_statuses) as became_closed, (case when he.was_story_points = 'No SPs' or he.was_story_points is null then 0 else he.was_story_points::numeric end) as was_story_points, (case when he.became_story_points = 'No SPs' or he.became_story_points is null then 0 else he.became_story_points::numeric end) as became_story_points from history_extraction he ),
Example of the output:
Specify the rules of addition/deletion of story points
decision_map as ( -- here we set the rules by which we decide whether add or subtract story points from sprint scope based on what happened with the ticket at that moment select * from ( -- was_my_sprint, became_my_sprint, was_closed, became_closed, was_story_points_multiplier, became_story_points_multiplier values (false, false, false, false, 0, 0), (false, false, false, true, 0, 0), (false, false, true, false, 0, 0), (false, false, true, true, 0, 0), (false, true, false, false, 0, 1), (false, true, false, true, 0, 0), (false, true, true, false, 0, 1), (false, true, true, true, 0, 0), (true, false, false, false, -1, 0), (true, false, false, true, -1, 0), (true, false, true, false, 0, 0), (true, false, true, true, 0, 0), (true, true, false, false, 0, 0), (true, true, false, true, -1, 0), (true, true, true, false, 0, 1), (true, true, true, true, 0, 0) ) s(was_my_sprint, became_my_sprint, was_closed, became_closed, was_story_points_multiplier, became_story_points_multiplier) ),
The output:
Specify how each history entry affects the story points of its Ticket (the story points that get added to the scope of the sprint)
operations as ( -- here we join what we got with the decision map to get actual additions and deletions select he.*, he.was_story_points * dm.was_story_points_multiplier as was_sp_operand, he.became_story_points * dm.became_story_points_multiplier as became_sp_operand from meaningful_history_extraction he join decision_map dm on he.was_my_sprint = dm.was_my_sprint and he.became_my_sprint = dm.became_my_sprint and he.was_closed = dm.was_closed and he.became_closed = dm.became_closed and (he.was_story_points <> 0 or he.became_story_points <> 0) -- always good to filter out what doesn't affect the scope order by start ),
Example of the output:
Combine the changes to Tickets into overall delta of the Sprint scope
totaladditions as ( -- here we combine the operands based on was_sps and became_sps into one delta - the change of sprint scope select was_sp_operand + became_sp_operand as delta, * from operations where was_sp_operand <> 0 or became_sp_operand <> 0 order by start -- always good to filter out what doesn't affect the scope ),
Example of the output:
Combine deltas into a cumulative scope size on each change
cumulative as ( -- here we sum up all changes to get the sprint scope size on each event select sum(delta) over (order by start) as cumulative, * from totaladditions order by start ),
Example of the output:
On this stage we have everything we need to just visualise the line from ‘cumulative’, because that is the amount of story points in sprint scope on each scope change. All the further data manipulations just serve the purpose of better visualisation
Generate hourly series of time points from start to finish of a sprint
generated_sprint_border_series as ( select date_trunc('hour', dd) dd from generate_series((select start_date from last_sprint limit 1)::timestamp, (select finish_date from last_sprint limit 1)::timestamp, '1 hour'::interval) dd )
Example of the output:
Select the Scope Dynamics by these series
select to_char(dd, 'dd/MM HH:MI Day') as "Time", ( select cumulative from cumulative where start < dd and dd < now() order by start desc limit 1) as "Story Points" from generated_sprint_border_series
Example of the output:
Full Code Recap
Then you should click “Save” to get back to the Visualisation Screen
Visualising the Metric
After saving, add the metric to your dashboard. See https://telescope-ai.atlassian.net/wiki/spaces/EPMDMO/pages/1056771045/Handle+Layout+of+Unit+Page#How-to-Handle-a-Card for details.
Checking the looks
Good job! That was a tough one! You really deserve a cup of something delicious, so go grab it to reward yourself!
Copyright © 2024 EPAM Systems, Inc. |
---|
All Rights Reserved. All information contained herein is, and remains the property of EPAM Systems, Inc. and/or its suppliers and is protected by international intellectual property law. Dissemination of this information or reproduction of this material is strictly forbidden, unless prior written permission is obtained from EPAM Systems, Inc. |