diff --git a/lib/gitlab/cycle_analytics/base_event_fetcher.rb b/lib/gitlab/cycle_analytics/base_event_fetcher.rb
index d4b2d665e593d515f4e0bfc99f813a29e6220ce9..8b4ccfd5363f34e6016e9e4b1ecd49e1ca47335a 100644
--- a/lib/gitlab/cycle_analytics/base_event_fetcher.rb
+++ b/lib/gitlab/cycle_analytics/base_event_fetcher.rb
@@ -1,15 +1,14 @@
 module Gitlab
   module CycleAnalytics
     class BaseEventFetcher
-      include MetricsTables
+      include BaseQuery
 
       attr_reader :projections, :query, :stage, :order
 
-      def initialize(fetcher:, options:, stage:)
-        @fetcher = fetcher
-        @project = fetcher.project
-        @options = options
+      def initialize(project:, stage:, options:)
+        @project = project
         @stage = stage
+        @options = options
       end
 
       def fetch
@@ -20,8 +19,6 @@ module Gitlab
         end.compact
       end
 
-      def custom_query(_base_query); end
-
       private
 
       def update_author!
@@ -31,7 +28,21 @@ module Gitlab
       end
 
       def event_result
-        @event_result ||= @fetcher.events.to_a
+        @event_result ||= ActiveRecord::Base.connection.exec_query(events_query.to_sql).to_a
+      end
+
+      def events_query
+        diff_fn = subtract_datetimes_diff(base_query, @options[:start_time_attrs], @options[:end_time_attrs])
+
+        base_query.project(extract_diff_epoch(diff_fn).as('total_time'), *projections).order(order.desc)
+      end
+
+      def order
+        @order || default_order
+      end
+
+      def default_order
+        @options[:start_time_attrs].is_a?(Array) ? @options[:start_time_attrs].first : @options[:start_time_attrs]
       end
 
       def serialize(_event)
diff --git a/lib/gitlab/cycle_analytics/base_query.rb b/lib/gitlab/cycle_analytics/base_query.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d560dca45c8abbec50ecf5790b9fa0d9715e01e8
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/base_query.rb
@@ -0,0 +1,31 @@
+module Gitlab
+  module CycleAnalytics
+    module BaseQuery
+      include MetricsTables
+      include Gitlab::Database::Median
+      include Gitlab::Database::DateTime
+
+      private
+
+      def base_query
+        @base_query ||= stage_query
+      end
+
+      def stage_query
+        query = mr_closing_issues_table.join(issue_table).on(issue_table[:id].eq(mr_closing_issues_table[:issue_id])).
+          join(issue_metrics_table).on(issue_table[:id].eq(issue_metrics_table[:issue_id])).
+          where(issue_table[:project_id].eq(@project.id)).
+          where(issue_table[:deleted_at].eq(nil)).
+          where(issue_table[:created_at].gteq(@options[:from]))
+
+        # Load merge_requests
+        query = query.join(mr_table, Arel::Nodes::OuterJoin).
+          on(mr_table[:id].eq(mr_closing_issues_table[:merge_request_id])).
+          join(mr_metrics_table).
+          on(mr_table[:id].eq(mr_metrics_table[:merge_request_id]))
+
+        query
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/cycle_analytics/base_stage.rb b/lib/gitlab/cycle_analytics/base_stage.rb
index c2605364ff05c46720c61687feb1afb63150659e..afec16d18183e2f0b6bfd588581a763e0991a65e 100644
--- a/lib/gitlab/cycle_analytics/base_stage.rb
+++ b/lib/gitlab/cycle_analytics/base_stage.rb
@@ -1,23 +1,17 @@
 module Gitlab
   module CycleAnalytics
     class BaseStage
-      include MetricsTables
-
-      attr_accessor :start_time_attrs, :end_time_attrs
+      include BaseQuery
 
       def initialize(project:, options:)
         @project = project
         @options = options
-        @fetcher = Gitlab::CycleAnalytics::MetricsFetcher.new(project: project,
-                                                              from: options[:from],
-                                                              branch: options[:branch],
-                                                              stage: self)
       end
 
       def event
-        @event ||= Gitlab::CycleAnalytics::Event[stage].new(fetcher: @fetcher,
-                                                            options: @options,
-                                                            stage: stage)
+        @event ||= Gitlab::CycleAnalytics::Event[name].new(project: @project,
+                                                           stage: name,
+                                                           options: event_options)
       end
 
       def events
@@ -29,17 +23,31 @@ module Gitlab
       end
 
       def title
-        stage.to_s.capitalize
+        name.to_s.capitalize
       end
 
       def median
-        @fetcher.median
+        cte_table = Arel::Table.new("cte_table_for_#{name}")
+
+        # Build a `SELECT` query. We find the first of the `end_time_attrs` that isn't `NULL` (call this end_time).
+        # Next, we find the first of the start_time_attrs that isn't `NULL` (call this start_time).
+        # We compute the (end_time - start_time) interval, and give it an alias based on the current
+        # cycle analytics stage.
+        interval_query = Arel::Nodes::As.new(
+          cte_table,
+          subtract_datetimes(base_query, @start_time_attrs, @end_time_attrs, name.to_s))
+
+        median_datetime(cte_table, interval_query, name)
+      end
+
+      def name
+        raise NotImplementedError.new("Expected #{self.name} to implement name")
       end
 
       private
 
-      def stage
-        class_name_for('Stage')
+      def event_options
+        @options.merge(start_time_attrs: @start_time_attrs, end_time_attrs: @end_time_attrs)
       end
     end
   end
diff --git a/lib/gitlab/cycle_analytics/code_stage.rb b/lib/gitlab/cycle_analytics/code_stage.rb
index 977d0d0210cf83691a3ba24b201e25c23d5c1e51..111c0e996332e337e93ff18805670b80c28d3aea 100644
--- a/lib/gitlab/cycle_analytics/code_stage.rb
+++ b/lib/gitlab/cycle_analytics/code_stage.rb
@@ -8,7 +8,7 @@ module Gitlab
         super(*args)
       end
 
-      def stage
+      def name
         :code
       end
 
diff --git a/lib/gitlab/cycle_analytics/issue_stage.rb b/lib/gitlab/cycle_analytics/issue_stage.rb
index 14e72c7ea48dc655b814df6616597b71905b2aa6..d320458d7fddd94586d6b5e8fc6da06d5459e4f0 100644
--- a/lib/gitlab/cycle_analytics/issue_stage.rb
+++ b/lib/gitlab/cycle_analytics/issue_stage.rb
@@ -9,7 +9,7 @@ module Gitlab
         super(*args)
       end
 
-      def stage
+      def name
         :issue
       end
 
diff --git a/lib/gitlab/cycle_analytics/metrics_fetcher.rb b/lib/gitlab/cycle_analytics/metrics_fetcher.rb
deleted file mode 100644
index 4115c092c0dc97b3261b577fdcf1e7487f555e0a..0000000000000000000000000000000000000000
--- a/lib/gitlab/cycle_analytics/metrics_fetcher.rb
+++ /dev/null
@@ -1,86 +0,0 @@
-module Gitlab
-  module CycleAnalytics
-    class MetricsFetcher
-      include Gitlab::Database::Median
-      include Gitlab::Database::DateTime
-      include MetricsTables
-
-      attr_reader :project
-
-      DEPLOYMENT_METRIC_STAGES = %i[production staging]
-
-      def initialize(project:, from:, branch:, stage:)
-        @project = project
-        @from = from
-        @branch = branch
-        @stage = stage
-      end
-
-      def median
-        cte_table = Arel::Table.new("cte_table_for_#{@stage.stage}")
-
-        # Build a `SELECT` query. We find the first of the `end_time_attrs` that isn't `NULL` (call this end_time).
-        # Next, we find the first of the start_time_attrs that isn't `NULL` (call this start_time).
-        # We compute the (end_time - start_time) interval, and give it an alias based on the current
-        # cycle analytics stage.
-        interval_query = Arel::Nodes::As.new(
-          cte_table,
-          subtract_datetimes(base_query_for(@stage.stage), @stage.start_time_attrs, @stage.end_time_attrs, @stage.stage.to_s))
-
-        median_datetime(cte_table, interval_query, @stage.stage)
-      end
-
-      def events
-        ActiveRecord::Base.connection.exec_query(events_query.to_sql)
-      end
-
-      private
-
-      def events_query
-        base_query = base_query_for(@stage.stage)
-
-        diff_fn = subtract_datetimes_diff(base_query, @stage.start_time_attrs, @stage.end_time_attrs)
-
-        @stage.event.custom_query(base_query)
-
-        base_query.project(extract_diff_epoch(diff_fn).as('total_time'), *@stage.event.projections).order(order.desc)
-      end
-
-      def order
-        @stage.event.order || default_order
-      end
-
-      def default_order
-        @stage.start_time_attrs.is_a?(Array) ? @stage.start_time_attrs.first : @stage.start_time_attrs
-      end
-
-      # Join table with a row for every <issue,merge_request> pair (where the merge request
-      # closes the given issue) with issue and merge request metrics included. The metrics
-      # are loaded with an inner join, so issues / merge requests without metrics are
-      # automatically excluded.
-      def base_query_for(name)
-        # Load issues
-        query = mr_closing_issues_table.join(issue_table).on(issue_table[:id].eq(mr_closing_issues_table[:issue_id])).
-          join(issue_metrics_table).on(issue_table[:id].eq(issue_metrics_table[:issue_id])).
-          where(issue_table[:project_id].eq(@project.id)).
-          where(issue_table[:deleted_at].eq(nil)).
-          where(issue_table[:created_at].gteq(@from))
-
-        query = query.where(build_table[:ref].eq(@branch)) if name == :test && @branch
-
-        # Load merge_requests
-        query = query.join(mr_table, Arel::Nodes::OuterJoin).
-          on(mr_table[:id].eq(mr_closing_issues_table[:merge_request_id])).
-          join(mr_metrics_table).
-          on(mr_table[:id].eq(mr_metrics_table[:merge_request_id]))
-
-        if DEPLOYMENT_METRIC_STAGES.include?(name)
-          # Limit to merge requests that have been deployed to production after `@from`
-          query.where(mr_metrics_table[:first_deployed_to_production_at].gteq(@from))
-        end
-
-        query
-      end
-    end
-  end
-end
diff --git a/lib/gitlab/cycle_analytics/plan_event_fetcher.rb b/lib/gitlab/cycle_analytics/plan_event_fetcher.rb
index 3e23c5644d3468c876b07205c2b74a68db24b9a7..88a8710dbe670e4f303f7c509407098dc4dc57d8 100644
--- a/lib/gitlab/cycle_analytics/plan_event_fetcher.rb
+++ b/lib/gitlab/cycle_analytics/plan_event_fetcher.rb
@@ -8,8 +8,10 @@ module Gitlab
         super(*args)
       end
 
-      def custom_query(base_query)
+      def events_query
         base_query.join(mr_diff_table).on(mr_diff_table[:merge_request_id].eq(mr_table[:id]))
+
+        super
       end
 
       private
diff --git a/lib/gitlab/cycle_analytics/plan_stage.rb b/lib/gitlab/cycle_analytics/plan_stage.rb
index f8c9b9c4495359cbca12c3b2369d1743c5e66b0e..a7164e5c5b785923c6147fc6dab262bb718ba5b8 100644
--- a/lib/gitlab/cycle_analytics/plan_stage.rb
+++ b/lib/gitlab/cycle_analytics/plan_stage.rb
@@ -9,7 +9,7 @@ module Gitlab
         super(*args)
       end
 
-      def stage
+      def name
         :plan
       end
 
diff --git a/lib/gitlab/cycle_analytics/production_helper.rb b/lib/gitlab/cycle_analytics/production_helper.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d693443bfa49012d5336541e09bb2aa8384d49c4
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/production_helper.rb
@@ -0,0 +1,9 @@
+module Gitlab
+  module CycleAnalytics
+    module ProductionHelper
+      def stage_query
+        super.where(mr_metrics_table[:first_deployed_to_production_at].gteq(@options[:from]))
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/cycle_analytics/production_stage.rb b/lib/gitlab/cycle_analytics/production_stage.rb
index 104c6d3fd303d142c44b4466c3c1ad1f67b808e4..eb221c68324e52271902ad0fd48d02d440fcd802 100644
--- a/lib/gitlab/cycle_analytics/production_stage.rb
+++ b/lib/gitlab/cycle_analytics/production_stage.rb
@@ -1,6 +1,8 @@
 module Gitlab
   module CycleAnalytics
     class ProductionStage < BaseStage
+      include ProductionHelper
+
       def initialize(*args)
         @start_time_attrs = issue_table[:created_at]
         @end_time_attrs = mr_metrics_table[:first_deployed_to_production_at]
@@ -8,13 +10,18 @@ module Gitlab
         super(*args)
       end
 
-      def stage
+      def name
         :production
       end
 
       def description
         "From issue creation until deploy to production"
       end
+
+      def query
+        # Limit to merge requests that have been deployed to production after `@from`
+        query.where(mr_metrics_table[:first_deployed_to_production_at].gteq(@from))
+      end
     end
   end
 end
diff --git a/lib/gitlab/cycle_analytics/review_stage.rb b/lib/gitlab/cycle_analytics/review_stage.rb
index c7bbd29693b028e61fb7e2fc26837c334e24d5ad..72ce1ed1e1640bb7b2464e31fe005f68174dfd95 100644
--- a/lib/gitlab/cycle_analytics/review_stage.rb
+++ b/lib/gitlab/cycle_analytics/review_stage.rb
@@ -8,7 +8,7 @@ module Gitlab
         super(*args)
       end
 
-      def stage
+      def name
         :review
       end
 
diff --git a/lib/gitlab/cycle_analytics/staging_event_fetcher.rb b/lib/gitlab/cycle_analytics/staging_event_fetcher.rb
index ea98e211ad6b57c99bf11b63761d6ec6f2f2ccb8..a34731a5fcdb9ced2a6b42a584f3b1995fcff152 100644
--- a/lib/gitlab/cycle_analytics/staging_event_fetcher.rb
+++ b/lib/gitlab/cycle_analytics/staging_event_fetcher.rb
@@ -14,8 +14,10 @@ module Gitlab
         super
       end
 
-      def custom_query(base_query)
+      def events_query
         base_query.join(build_table).on(mr_metrics_table[:pipeline_id].eq(build_table[:commit_id]))
+
+        super
       end
 
       private
diff --git a/lib/gitlab/cycle_analytics/staging_stage.rb b/lib/gitlab/cycle_analytics/staging_stage.rb
index 079b26760bb32496953dbd769a922436620fef28..398c1b5989ac84099520c3082e07ecb4863e163c 100644
--- a/lib/gitlab/cycle_analytics/staging_stage.rb
+++ b/lib/gitlab/cycle_analytics/staging_stage.rb
@@ -1,6 +1,8 @@
 module Gitlab
   module CycleAnalytics
     class StagingStage < BaseStage
+      include ProductionHelper
+
       def initialize(*args)
         @start_time_attrs = mr_metrics_table[:merged_at]
         @end_time_attrs = mr_metrics_table[:first_deployed_to_production_at]
@@ -8,7 +10,7 @@ module Gitlab
         super(*args)
       end
 
-      def stage
+      def name
         :staging
       end
 
diff --git a/lib/gitlab/cycle_analytics/summary/commit.rb b/lib/gitlab/cycle_analytics/summary/commit.rb
index ec3c067c0bed5d04ef61e646ffa19b46c4c40219..61a507621643ae64f8dfe546d42d3f952c6fc84a 100644
--- a/lib/gitlab/cycle_analytics/summary/commit.rb
+++ b/lib/gitlab/cycle_analytics/summary/commit.rb
@@ -23,8 +23,11 @@ module Gitlab
           cmd << "--after=#{@from.iso8601}"
           cmd << sha
 
-          raw_output = IO.popen(cmd) { |io| io.read }
-          raw_output.lines.count
+          output, status = Gitlab::Popen.popen(cmd) { |io| io.read }
+
+          raise IOError, output unless status.zero?
+
+          output.lines.count
         end
 
         def ref
diff --git a/lib/gitlab/cycle_analytics/test_stage.rb b/lib/gitlab/cycle_analytics/test_stage.rb
index a105e5f2b1f5db0de21c0ef03aed9a08b785c8b9..7e59745ffef4df7dc2952e4502ba9ad733383aa4 100644
--- a/lib/gitlab/cycle_analytics/test_stage.rb
+++ b/lib/gitlab/cycle_analytics/test_stage.rb
@@ -8,13 +8,21 @@ module Gitlab
         super(*args)
       end
 
-      def stage
+      def name
         :test
       end
 
       def description
         "Total test time for all commits/merges"
       end
+
+      def stage_query
+        if @options[:branch]
+          super.where(build_table[:ref].eq(@options[:branch]))
+        else
+          super
+        end
+      end
     end
   end
 end
diff --git a/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb b/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb
index 725f9a558f5ea679b2cc71a09eb41fd77d590d66..03b013ffae8da5679a77a0382d1fb6d17853c295 100644
--- a/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb
+++ b/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb
@@ -8,10 +8,11 @@ shared_examples 'default query config' do
                                                stage: stage_name)
   end
 
-  let(:event) { described_class.new(fetcher: fetcher, options: {}, stage: stage_name) }
+  let(project)
+  let(:event) { described_class.new(project: project, stage: stage_name, options: {}) }
 
   it 'has the stage attribute' do
-    expect(event.stage).not_to be_nil
+    expect(event.name).not_to be_nil
   end
 
   it 'has the projection attributes' do