Add CountFrameworksWithRequirementsMetric

What does this MR do and why?

Add CountFrameworksWithRequirementsMetric

Changelog: other EE: true

References

Please include cross links to any resources that are relevant to this MR. This will give reviewers and future readers helpful context to give an efficient review of the changes introduced.

MR acceptance checklist

Please evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Screenshots or screen recordings

Screenshots are required for UI changes, and strongly recommended for all other merge requests.

Before After

Query plan:

SELECT COUNT(DISTINCT "compliance_management_frameworks"."id") 
FROM "compliance_management_frameworks" 
INNER JOIN "compliance_requirements" 
  ON "compliance_requirements"."framework_id" = "compliance_management_frameworks"."id"

How to set up and validate locally

Setup local compliance frameworks with/without requirements and run this:

metric_frameworks = Gitlab::Usage::Metrics::Instrumentations::CountFrameworksWithRequirementsMetric.new({
  time_frame: 'all'
})
puts "SQL query for frameworks with requirements: #{metric_frameworks.to_sql}"
puts "Value: #{metric_frameworks.value}"

Where time_frame can be 'all' '7d' '28d'

or use this script:

code
test_id = "test_#{Time.now.to_i}"

created_records = { 
  requirements: [], 
  frameworks: [],
  namespace: nil
}

begin
  puts "=== Testing CountFrameworksWithRequirementsMetric ==="
  puts "Using test identifier: #{test_id}"
  
  organization = Organizations::Organization.first
  namespace = Group.create!(
    name: "Test Group #{test_id}",
    path: "test-group-#{test_id}",
    organization: organization
  )
  created_records[:namespace] = namespace
  
  frameworks = []
  
  framework1 = ComplianceManagement::Framework.create!(
    name: "Framework With Req #{test_id}",
    description: "Framework with requirements",
    color: "#FF0000",
    namespace: namespace
  )
  frameworks << framework1
  
  framework2 = ComplianceManagement::Framework.create!(
    name: "Framework With Req Old #{test_id}",
    description: "Framework with requirements (older)",
    color: "#00FF00",
    namespace: namespace,
    created_at: 14.days.ago,
    updated_at: 14.days.ago
  )
  frameworks << framework2
  
  framework3 = ComplianceManagement::Framework.create!(
    name: "Framework With Req Oldest #{test_id}",
    description: "Framework with requirements (oldest)",
    color: "#0000FF",
    namespace: namespace,
    created_at: 30.days.ago,
    updated_at: 30.days.ago
  )
  frameworks << framework3
  
  framework4 = ComplianceManagement::Framework.create!(
    name: "Framework No Req #{test_id}",
    description: "Framework without requirements",
    color: "#FFFF00",
    namespace: namespace
  )
  frameworks << framework4
  
  created_records[:frameworks] = frameworks
  frameworks.each { |f| puts "- #{f.name} (ID: #{f.id})" }
  
  requirements = []
  
  req1 = ComplianceManagement::ComplianceFramework::ComplianceRequirement.create!(
    framework: framework1,
    namespace: namespace,
    name: "Requirement for F1 #{test_id}",
    description: "Current requirement"
  )
  requirements << req1
  
  req2 = ComplianceManagement::ComplianceFramework::ComplianceRequirement.create!(
    framework: framework2,
    namespace: namespace,
    name: "Requirement for F2 #{test_id}",
    description: "14-day old requirement",
    created_at: 14.days.ago,
    updated_at: 14.days.ago
  )
  requirements << req2
  
  req3 = ComplianceManagement::ComplianceFramework::ComplianceRequirement.create!(
    framework: framework3,
    namespace: namespace,
    name: "Requirement for F3 #{test_id}",
    description: "30-day old requirement",
    created_at: 30.days.ago,
    updated_at: 30.days.ago
  )
  requirements << req3
  
  created_records[:requirements] = requirements
  
  class TestCountFrameworksWithRequirementsMetric
    def initialize(framework_ids, time_frame)
      @framework_ids = framework_ids
      @time_frame = time_frame
    end
    
    def value
      case @time_frame
      when 'all'
        ComplianceManagement::Framework
          .where(id: @framework_ids)
          .joins(:compliance_requirements)
          .distinct
          .count
      when '28d'
        ComplianceManagement::Framework
          .where(id: @framework_ids)
          .where(created_at: 28.days.ago..Time.current)
          .joins(:compliance_requirements)
          .distinct
          .count
      when '7d'
        ComplianceManagement::Framework
          .where(id: @framework_ids)
          .where(created_at: 7.days.ago..Time.current)
          .joins(:compliance_requirements)
          .distinct
          .count
      end
    end
  end
  
  puts "\n=== Testing Metrics ==="
  
  time_frames = %w[all 28d 7d]
  
  time_frames.each do |time_frame|
    expected = case time_frame
               when 'all'
                 3  # All 3 frameworks with requirements
               when '28d'
                 2  # Framework 1 and 2 (excluding the 30-day old one)
               when '7d'
                 1  # Only framework 1 (current)
               end
    
    test_metric = TestCountFrameworksWithRequirementsMetric.new(
      frameworks.map(&:id),
      time_frame
    )
    value = test_metric.value
    
    puts "\n---------------------------"
    puts "Time frame: #{time_frame}"
    puts "Value: #{value} (Expected: #{expected})"
    puts "Status: #{value == expected ? 'PASS ✓' : 'FAIL ✗'}"
    
    real_metric = Gitlab::Usage::Metrics::Instrumentations::CountFrameworksWithRequirementsMetric.new({
      time_frame: time_frame
    })
    
    puts "\nFor comparison, actual metric implementation returns:"
    puts "SQL: #{real_metric.to_sql}"
    puts "Value includes your test data and any other data in the database"
  end
  
  puts "\n=== Test Complete ==="
  
rescue => e
  puts "Error: #{e.message}"
  puts e.backtrace.join("\n")
ensure
  puts "\n=== Cleaning up test data ==="
  
  created_records[:requirements].each(&:destroy!) if created_records[:requirements]
  created_records[:frameworks].each(&:destroy!) if created_records[:frameworks]
  created_records[:namespace]&.destroy!
  
  puts "All test data cleaned up"
end

puts "Script completed"
Edited by Andrew Jung

Merge request reports

Loading