Adjust Estimate According to Assignee

Adjust Estimate According to Assignee

Required apps

Power Scripts™ for Jira (server)

Level: INTERMEDIATE

Problem

Estimating issues is a difficult task, especially in Agile methodologies. Since you rarely know who is going to end up with a task, you can only give a rough estimate on how much time it should take your average team member to do it. But then you might have new people in your team, who might require more time to figure out what's going on inside before they make the changes. Moreover, even older team members can take a different amount of time to solve an issue.

Solution

One option is to "customize" the estimate. Let's say we have a task estimated at 1d. Some may need 1d 4h to solve it, some might do it in 6h. If we can keep track of the error margin for each user and average that across the issues he/she has worked on, it would be possible to give a better starting estimate for future tasks. If a user is constantly taking longer than the average by, let's say 20%, then we could anticipate and raise the estimate by 20% for him/her.

To do so, we'll add 2 post-functions:

  1. Start Progress - when a user starts progress on an issue, we should re-calculate the remaining estimate to conform to his/her velocity. A user's velocity is the factor by which we should multiply the original estimate to give a better approximation.
  2. Resolve Issue - when an issue is resolved, we'll add the difference between the time actually spent working on the issue and what we calculated when progress started, then we'll include this result in future approximations.

We'll keep track of the parameters by storing them as user properties and we'll hold 2 values:

  1. user's velocity
  2. number of iterations - each time we alter the velocity, we increment this value

Each user starts off with a velocity of 1 and 0 iterations.

Step 1 - Starting Progress - recalculating the estimate

When a user starts progress on an issue, we'll take the original estimate, multiply it by the user's velocity and put the result in the remaining estimate. However, one might work on an issue intermittently, so we'll do this only if there was no time logged on the issue.

Tip

You can also do this step (with minor changes) when the issue is re-assigned. To allow some flexibility, we'll write all the necessary functions in a separate file that we can include from other SIL™ scripts.

Start Progress Post Function
include "common_velocity.incl"; // our "auto-estimate library"
updateRemaining(key); 


Step 2 - Resolving an Issue - recalculating the velocity

When an issue is resolved, we need to calculate the approximation error and include it in the velocity to be used for future estimates.

Resolve Issue Post Function
include "common_velocity.incl"; // our "auto-estimate library"
adjustVelocity(key); 


Step 3 - Adding the Environment Configuration

We'll use the SIL™ Environment to hold the default velocity (1) and user properties which store the velocity and number of iterations.

sil.properties
default.velocity = 1
user.velocity = jjup.autoestimate.user.velocity
velocity.samples = jjup.autoestimate.velocity.samples


Step 4 - Adding the "library"

As we said earlier, we'll put all the logic in one file which we can include everywhere else we decide to re-estimate (listeners, services, etc). All you need to do is save the file below in your silprograms folder.

common_velocity.incl
function getUserVelocity(string user){
	if(! userExists(user)){
		number DEFAULT_VELOCITY = silEnv("default.velocity");
		logPrint("DEBUG", "User " + user + " does not exist. Cannot calculate velocity. Using default " + DEFAULT_VELOCITY);
		return DEFAULT_VELOCITY;
	}
	number velocity = getUserProperty(user, silEnv("user.velocity"));

	if(isNull(velocity)){
		string DEFAULT_VELOCITY = silEnv("default.velocity");
		setUserProperty(user, silEnv("user.velocity"), DEFAULT_VELOCITY);
		velocity = DEFAULT_VELOCITY;
	}
	return velocity;
}


function getNoVelocitySamples(string user){
	if(! userExists(user)){
		logPrint("DEBUG", "User " + user + " does not exist. Cannot get number of velocity samples");
		return 0;
	}
	number samples = getUserProperty(user, silEnv("velocity.samples"));

	if(isNull(samples)){
		setUserProperty(user, silEnv("velocity.samples"), 0);
		samples = 0;
	}
	return samples;
}


function setNoSamples(string user, number noSamples){
	if(! userExists(user)){
		logPrint("DEBUG", "User " + user + " does not exist. Cannot set velocity samples.");
		return ;
	}
	setUserProperty(user, silEnv("velocity.samples"), noSamples);
}


function setUserVelocity(string user, number velocity){
	if(! userExists(user)){
		logPrint("DEBUG", "User " + user + " does not exist. Cannot set velocity.");
		return ;
	}
	setUserProperty(user, silEnv("user.velocity"), velocity);
}


function getWorkForUser(string user, string issKey){
	number [] ids = getWorklogIdsForUser(user, issKey);
	interval intvl = "0h";
	if(isNull(ids) || size(ids) == 0){
		return intvl;
	}
	for(number wlid in ids){
		intvl += getWorklogLoggedHours(wlid);
	}
	return intvl;
}


function isSoloWork(string user, string issKey){
	return %issKey%.spent == getWorkForUser(user, issKey);
}


function adjustVelocity(string issKey){
	string user = %issKey%.assignee;
	if(!isSoloWork(user, issKey)){
		logPrint("DEBUG", "User" + user + " did not work all by himself on issue " + issKey + ". Cannot update velocity");
		return;
	}
	number noSamples = getNoVelocitySamples(user);
	number velocity = getUserVelocity(user);
	number estimateWithVelocity = %issKey%.originalEstimate["TOMILLIS"] * velocity;
	number currentRatio = %issKey%.spent["TOMILLIS"] /estimateWithVelocity;
	number newVelo = (velocity * noSamples + currentRatio) / (noSamples + 1);
 
	setUserVelocity(user, newVelo);
	setNoSamples(user, noSamples + 1);
	logPrint("INFO", "Updated user " + user + " velocity from " + velocity + " to " + newVelo + " based on " + (noSamples+1) + " samples");
}


function updateRemaining(string issKey){
	if(%issKey%.spent > "0d" ){
		logPrint("DEBUG", "Work already logged. Not altering remaining estimate.");
		return;
	}
	number velocity = getUserVelocity(%issKey%.assignee);
	number remainingMillis = %issKey%.originalEstimate["TOMILLIS"];
	%issKey%.remaining = remainingMillis * velocity;
} 

Tip

You can further tweak the functions provided to account for other users that might have worked on the issue.

See also