From 1c9be5d7f53bdd7f28988eab154a504659aabcfa Mon Sep 17 00:00:00 2001
From: Pavel Shutsin <pshutsin@gitlab.com>
Date: Tue, 1 Oct 2019 13:44:21 +0300
Subject: [PATCH] Limit Productivity Analytics date filtering

Users shouldn't be able to select dates for filtering earlier
than the feature was introduced.
---
 .../application_setting_implementation.rb     |  3 +-
 .../32052-add-pa-start-date-limit.yml         |  5 +++
 ...8_add_productivity_analytics_start_date.rb | 11 ++++++
 ..._fill_productivity_analytics_start_date.rb | 31 +++++++++++++++
 db/schema.rb                                  |  1 +
 .../finders/productivity_analytics_finder.rb  | 16 +++++++-
 ee/app/models/productivity_analytics.rb       |  4 ++
 .../productivity_analytics/show.html.haml     |  2 +-
 .../productivity_analytics_finder_spec.rb     | 20 ++++++++++
 ee/spec/models/productivity_analytics_spec.rb | 15 ++++++-
 ..._productivity_analytics_start_date_spec.rb | 39 +++++++++++++++++++
 11 files changed, 142 insertions(+), 5 deletions(-)
 create mode 100644 changelogs/unreleased/32052-add-pa-start-date-limit.yml
 create mode 100644 db/migrate/20191004080818_add_productivity_analytics_start_date.rb
 create mode 100644 db/migrate/20191004081520_fill_productivity_analytics_start_date.rb
 create mode 100644 spec/migrations/fill_productivity_analytics_start_date_spec.rb

diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index 9119f8766ebe0..339563b7e4be3 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -136,7 +136,8 @@ def defaults
         snowplow_iglu_registry_url: nil,
         custom_http_clone_url_root: nil,
         pendo_enabled: false,
-        pendo_url: nil
+        pendo_url: nil,
+        productivity_analytics_start_date: Time.now
       }
     end
 
diff --git a/changelogs/unreleased/32052-add-pa-start-date-limit.yml b/changelogs/unreleased/32052-add-pa-start-date-limit.yml
new file mode 100644
index 0000000000000..fa2cf796edbb4
--- /dev/null
+++ b/changelogs/unreleased/32052-add-pa-start-date-limit.yml
@@ -0,0 +1,5 @@
+---
+title: Add productivity analytics merge date filtering limit
+merge_request: 32052
+author:
+type: fixed
diff --git a/db/migrate/20191004080818_add_productivity_analytics_start_date.rb b/db/migrate/20191004080818_add_productivity_analytics_start_date.rb
new file mode 100644
index 0000000000000..287b0755bc190
--- /dev/null
+++ b/db/migrate/20191004080818_add_productivity_analytics_start_date.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class AddProductivityAnalyticsStartDate < ActiveRecord::Migration[5.2]
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  def change
+    add_column :application_settings, :productivity_analytics_start_date, :datetime_with_timezone
+  end
+end
diff --git a/db/migrate/20191004081520_fill_productivity_analytics_start_date.rb b/db/migrate/20191004081520_fill_productivity_analytics_start_date.rb
new file mode 100644
index 0000000000000..9432cd6870883
--- /dev/null
+++ b/db/migrate/20191004081520_fill_productivity_analytics_start_date.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+# Expected migration duration: 1 minute
+class FillProductivityAnalyticsStartDate < ActiveRecord::Migration[5.2]
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  disable_ddl_transaction!
+
+  def up
+    add_concurrent_index :merge_request_metrics, :merged_at,
+                         where: "merged_at > '2019-09-01' AND commits_count IS NOT NULL",
+                         name: 'fill_productivity_analytics_start_date_tmp_index'
+
+    execute(
+      <<SQL
+        UPDATE application_settings
+        SET productivity_analytics_start_date = COALESCE((SELECT MIN(merged_at) FROM merge_request_metrics
+            WHERE merged_at > '2019-09-01' AND commits_count IS NOT NULL), NOW())
+SQL
+    )
+
+    remove_concurrent_index :merge_request_metrics, :merged_at,
+                            name: 'fill_productivity_analytics_start_date_tmp_index'
+  end
+
+  def down
+    execute('UPDATE application_settings SET productivity_analytics_start_date = NULL')
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 36f82f9913b7a..1c3c024743fc2 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -350,6 +350,7 @@
     t.string "eks_access_key_id", limit: 128
     t.string "encrypted_eks_secret_access_key_iv", limit: 255
     t.text "encrypted_eks_secret_access_key"
+    t.datetime_with_timezone "productivity_analytics_start_date"
     t.index ["custom_project_templates_group_id"], name: "index_application_settings_on_custom_project_templates_group_id"
     t.index ["file_template_project_id"], name: "index_application_settings_on_file_template_project_id"
     t.index ["instance_administration_project_id"], name: "index_applicationsettings_on_instance_administration_project_id"
diff --git a/ee/app/finders/productivity_analytics_finder.rb b/ee/app/finders/productivity_analytics_finder.rb
index 88d4dec285a86..ff18d656ff564 100644
--- a/ee/app/finders/productivity_analytics_finder.rb
+++ b/ee/app/finders/productivity_analytics_finder.rb
@@ -37,10 +37,22 @@ def by_merged_at(items)
     return items unless params[:merged_at_after] || params[:merged_at_before]
 
     items = items.joins(:metrics)
-    items = items.where(metrics_table[:merged_at].gteq(params[:merged_at_after])) if params[:merged_at_after]
-    items = items.where(metrics_table[:merged_at].lteq(params[:merged_at_before])) if params[:merged_at_before]
+    items = items.where(metrics_table[:merged_at].gteq(merged_at_between[:from])) if merged_at_between[:from]
+    items = items.where(metrics_table[:merged_at].lteq(merged_at_between[:to])) if merged_at_between[:to]
 
     items
   end
   # rubocop: enable CodeReuse/ActiveRecord
+
+  def merged_at_between
+    @merged_at_between ||= begin
+      boundaries = { from: params[:merged_at_after], to: params[:merged_at_before] }
+
+      if ProductivityAnalytics.start_date && ProductivityAnalytics.start_date > boundaries[:from]
+        boundaries[:from] = ProductivityAnalytics.start_date
+      end
+
+      boundaries
+    end
+  end
 end
diff --git a/ee/app/models/productivity_analytics.rb b/ee/app/models/productivity_analytics.rb
index 760e810358b76..63615d6b13b1f 100644
--- a/ee/app/models/productivity_analytics.rb
+++ b/ee/app/models/productivity_analytics.rb
@@ -16,6 +16,10 @@ class ProductivityAnalytics
   METRIC_TYPES = METRIC_COLUMNS.keys.freeze
   DEFAULT_TYPE = 'days_to_merge'.freeze
 
+  def self.start_date
+    ApplicationSetting.current&.productivity_analytics_start_date
+  end
+
   def initialize(merge_requests:, sort: nil)
     @merge_requests = merge_requests.joins(:metrics)
     @sort = sort
diff --git a/ee/app/views/analytics/productivity_analytics/show.html.haml b/ee/app/views/analytics/productivity_analytics/show.html.haml
index 8ecacd091c80b..be1d78e6cab20 100644
--- a/ee/app/views/analytics/productivity_analytics/show.html.haml
+++ b/ee/app/views/analytics/productivity_analytics/show.html.haml
@@ -5,5 +5,5 @@
     .js-group-project-select-container
     .js-search-bar.filter-container.hide
       = render 'shared/issuable/search_bar', type: :productivity_analytics
-    .js-timeframe-container
+    .js-timeframe-container{ data: { start_date: ProductivityAnalytics.start_date } }
   .js-productivity-analytics-app-container{ data: { endpoint: analytics_productivity_analytics_path, empty_state_svg_path: image_path('illustrations/productivity-analytics-empty-state.svg'), no_access_svg_path: image_path('illustrations/analytics/no-access.svg') } }
diff --git a/ee/spec/finders/productivity_analytics_finder_spec.rb b/ee/spec/finders/productivity_analytics_finder_spec.rb
index a455f2f95d86c..2c0e54c2e980f 100644
--- a/ee/spec/finders/productivity_analytics_finder_spec.rb
+++ b/ee/spec/finders/productivity_analytics_finder_spec.rb
@@ -44,6 +44,12 @@
     end
 
     context 'allows to filter by merged_at' do
+      let(:pa_start_date) { 2.years.ago }
+
+      before do
+        allow(ProductivityAnalytics).to receive(:start_date).and_return(pa_start_date)
+      end
+
       around do |example|
         Timecop.freeze { example.run }
       end
@@ -76,6 +82,20 @@
           expect(subject.execute).to match_array([short_mr])
         end
       end
+
+      context 'with merged_at_after earlier than PA start date' do
+        let(:search_params) do
+          { merged_at_after: 3.years.ago.to_s }
+        end
+
+        it 'uses start_date as filter value' do
+          metrics_data = { merged_at: (2.years + 1.day).ago }
+          create(:merge_request, :merged, :with_productivity_metrics, created_at: 800.days.ago, metrics_data: metrics_data)
+          long_mr
+
+          expect(subject.execute).to match_array([long_mr])
+        end
+      end
     end
   end
 end
diff --git a/ee/spec/models/productivity_analytics_spec.rb b/ee/spec/models/productivity_analytics_spec.rb
index 7524481503507..1c9886b9dfe05 100644
--- a/ee/spec/models/productivity_analytics_spec.rb
+++ b/ee/spec/models/productivity_analytics_spec.rb
@@ -4,7 +4,7 @@
 
 describe ProductivityAnalytics do
   describe 'metrics data' do
-    let(:analytics) { described_class.new(merge_requests: finder_mrs, sort: custom_sort) }
+    subject(:analytics) { described_class.new(merge_requests: finder_mrs, sort: custom_sort) }
 
     let(:finder_mrs) { ProductivityAnalyticsFinder.new(create(:admin), finder_options).execute }
     let(:finder_options) { { state: 'merged' } }
@@ -221,4 +221,17 @@
       end
     end
   end
+
+  describe '.start_date' do
+    subject(:start_date) { described_class.start_date }
+
+    let(:application_setting) do
+      instance_double('ApplicationSetting', productivity_analytics_start_date: 'mocked-start-date')
+    end
+
+    it 'delegates to ApplicationSetting' do
+      allow(ApplicationSetting).to receive('current').and_return(application_setting)
+      expect(start_date).to eq 'mocked-start-date'
+    end
+  end
 end
diff --git a/spec/migrations/fill_productivity_analytics_start_date_spec.rb b/spec/migrations/fill_productivity_analytics_start_date_spec.rb
new file mode 100644
index 0000000000000..7cbba9ef20e79
--- /dev/null
+++ b/spec/migrations/fill_productivity_analytics_start_date_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'migrate', '20191004081520_fill_productivity_analytics_start_date.rb')
+
+describe FillProductivityAnalyticsStartDate, :migration do
+  let(:settings_table) { table('application_settings') }
+  let(:metrics_table) { table('merge_request_metrics') }
+
+  before do
+    settings_table.create!
+  end
+
+  context 'with NO productivity analytics data available' do
+    it 'sets start_date to NOW' do
+      expect { migrate! }.to change {
+        settings_table.first&.productivity_analytics_start_date
+      }.to(be_like_time(Time.now))
+    end
+  end
+
+  context 'with productivity analytics data available' do
+    before do
+      ActiveRecord::Base.transaction do
+        ActiveRecord::Base.connection.execute('ALTER TABLE merge_request_metrics DISABLE TRIGGER ALL')
+        metrics_table.create!(merged_at: Time.parse('2019-09-09'), commits_count: nil, merge_request_id: 3)
+        metrics_table.create!(merged_at: Time.parse('2019-10-10'), commits_count: 5, merge_request_id: 1)
+        metrics_table.create!(merged_at: Time.parse('2019-11-11'), commits_count: 10, merge_request_id: 2)
+        ActiveRecord::Base.connection.execute('ALTER TABLE merge_request_metrics ENABLE TRIGGER ALL')
+      end
+    end
+
+    it 'set start_date to earliest merged_at value with PA data available' do
+      expect { migrate! }.to change {
+        settings_table.first&.productivity_analytics_start_date
+      }.to(be_like_time(Time.parse('2019-10-10')))
+    end
+  end
+end
-- 
GitLab