[go: up one dir, main page]

Throughput chart MVC (BE)

Summary

This is the backend implementation issue for the Throughput Analytics feature.

Proposal

See the proposal in the implementation issue.

This MVC feature is at the project level.

Proof of concept

Please see the proof of concept that was created, built on the GitLab API:

throughput_by_month.rb ``` require 'date' require 'json' require 'net/http'

Purpose

Get data on throughput (number of merged merge requests).

Instructions

Customize access_token and query_params in the initialize method.

class ThroughputByMonth

attr_accessor(
  :access_token,
  :missing_merged_at_merge_request_iids,
  :query_params,
)

GITLAB_BOT_USER_ID = 1786152
GITLAB_ORG_GROUP_ID = 9970 # gitlab-org

def initialize
  self.access_token = 'PAT_GOES_HERE'
  self.missing_merged_at_merge_request_iids = []
  self.query_params = {
    # Documentation at https://docs.gitlab.com/ee/api/merge_requests.html#list-merge-requests
    # author_username: 'djensen',
    # created_after: '2020-01-01', # for testing only
    labels: 'group::analytics,frontend',
  }
end

def data
  {
    average_throughput: average_throughput_per_month,
    by_yearmonth: metrics_by_yearmonth.map do |yearmonth, metrics|
      [
        yearmonth,
        {
          count: metrics[:merged_merge_request_count],
          mttm_days: mttm_days(metrics),
        }
      ]
    end.to_h
  }
end

private

  def average_throughput_per_month
    throughputs = metrics_by_yearmonth.map { |yearmonth, metrics| metrics[:merged_merge_request_count] }
    total_throughput = throughputs.sum
    month_count = metrics_by_yearmonth.keys.size
    (total_throughput.to_f / month_count).round(1)
  end

  def build_query_string(params)
    query_string_default_params.merge(params).map do |key, value|
      "#{key}=#{value}"
    end.join('&')
  end

  def current_month_start_date
    @current_month_start_date ||= (Date.today - Date.today.mday + 1)
  end

  def time_from_string(string)
    DateTime.parse(string).to_time
  end

  def earliest_allowed_merged_at
    @earliest_allowed_merged_at ||= (current_month_start_date - 365).to_time
  end

  def get_all_response_results(url, params)
    results = []
    target_page = 1

    loop do
      params[:page] = target_page
      response = get_response(url, params)
      results.concat(JSON.parse(response.body))
      break if target_page >= response.header['X-Total-Pages'].to_i # Documented at https://docs.gitlab.com/ee/api/README.html#other-pagination-headers

      target_page += 1
    end

    return results
  end

  def get_response(url, params)
    sleep(0.1)
    query_string = build_query_string(params)
    uri = URI("#{url}?#{query_string}")
    request = Net::HTTP::Get.new(uri)
    Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
      http.request(request)
    end
  end

  def group_merge_requests_url
    @group_merge_requests_url ||= "https://gitlab.com/api/v4/groups/#{GITLAB_ORG_GROUP_ID}/merge_requests"
  end

  def increment_metrics_for_merge_request(merge_request:, metrics_by_yearmonth:)
    created_at = time_from_string(merge_request['created_at'])
    merged_at = merge_request_merged_at(merge_request)
    return if merged_at < earliest_allowed_merged_at
    # return if merged_at > current_month_start_date

    merged_yearmonth = yearmonth(merged_at)
    metrics_by_yearmonth[merged_yearmonth] = metrics_default unless metrics_by_yearmonth.has_key?(merged_yearmonth)
    metrics_by_yearmonth[merged_yearmonth][:merged_merge_request_count] += 1
    metrics_by_yearmonth[merged_yearmonth][:merge_request_iids] << merge_request['iid']
    metrics_by_yearmonth[merged_yearmonth][:total_time_to_merge] += (merged_at - created_at)
  end

  def last_complete_yearmonth
    @last_complete_yearmonth ||= team_metrics_by_yearmonth.sort.to_h.keys.last
  end

  def merge_request_merged_at(merge_request)
    merged_at = if merge_request['merged_at']
      merge_request['merged_at']
    else # missing merged_at per https://gitlab.com/gitlab-org/gitlab/issues/26911
      self.missing_merged_at_merge_request_iids << merge_request['id']
      merge_request['updated_at'] # closest proxy
    end
    time_from_string(merged_at)
  end

  def merged_merge_requests
    params = query_params.merge({ state: 'merged' })
    get_all_response_results(group_merge_requests_url, params)
  end

  def metrics_by_yearmonth
    return @metrics_by_yearmonth if @metrics_by_yearmonth
    
    @metrics_by_yearmonth = {}
    merged_merge_requests.each do |merge_request|
      increment_metrics_for_merge_request(merge_request: merge_request, metrics_by_yearmonth: metrics_by_yearmonth)
    end
    @metrics_by_yearmonth = @metrics_by_yearmonth.sort.to_h
  end

  def metrics_default
    {
      # authored_merge_request_count: 0,
      merged_merge_request_count: 0,
      merge_request_iids: [],
      total_time_to_merge: 0,
    }
  end

  def mttm_days(metrics)
    mttm_seconds = metrics[:total_time_to_merge] / metrics[:merge_request_iids].size
    (mttm_seconds / seconds_per_day).round(1)
  end

  def query_string_default_params
    {
      per_page: 50, # Max is 100
      private_token: access_token,
    }
  end

  def seconds_per_day
    @seconds_per_day ||= 60 * 60 * 24
  end

  def yearmonth(datetime)
    date = datetime.to_date
    date.strftime('%Y%m').to_i
  end

end

throughput = ThroughputByMonth.new puts throughput.data

</details>
Edited by Nick Post