diff options
Diffstat (limited to 'spec/lib/gitlab/usage')
-rw-r--r-- | spec/lib/gitlab/usage/docs/renderer_spec.rb | 21 | ||||
-rw-r--r-- | spec/lib/gitlab/usage/docs/value_formatter_spec.rb | 26 | ||||
-rw-r--r-- | spec/lib/gitlab/usage/metric_definition_spec.rb | 23 | ||||
-rw-r--r-- | spec/lib/gitlab/usage/metric_spec.rb | 8 | ||||
-rw-r--r-- | spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb | 256 |
5 files changed, 323 insertions, 11 deletions
diff --git a/spec/lib/gitlab/usage/docs/renderer_spec.rb b/spec/lib/gitlab/usage/docs/renderer_spec.rb new file mode 100644 index 00000000000..e62861cd677 --- /dev/null +++ b/spec/lib/gitlab/usage/docs/renderer_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Usage::Docs::Renderer do + describe 'contents' do + let(:dictionary_path) { Gitlab::Usage::Docs::Renderer::DICTIONARY_PATH } + let(:items) { Gitlab::Usage::MetricDefinition.definitions } + + it 'generates dictionary for given items' do + generated_dictionary = described_class.new(items).contents + generated_dictionary_keys = RDoc::Markdown + .parse(generated_dictionary) + .table_of_contents + .select { |metric_doc| metric_doc.level == 2 && !metric_doc.text.start_with?('info:') } + .map(&:text) + + expect(generated_dictionary_keys).to match_array(items.keys) + end + end +end diff --git a/spec/lib/gitlab/usage/docs/value_formatter_spec.rb b/spec/lib/gitlab/usage/docs/value_formatter_spec.rb new file mode 100644 index 00000000000..ceb00867c95 --- /dev/null +++ b/spec/lib/gitlab/usage/docs/value_formatter_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Usage::Docs::ValueFormatter do + describe '.format' do + using RSpec::Parameterized::TableSyntax + where(:key, :value, :expected_value) do + :group | 'growth::product intelligence' | '`growth::product intelligence`' + :data_source | 'redis' | 'Redis' + :data_source | 'ruby' | 'Ruby' + :introduced_by_url | 'http://test.com' | '[Introduced by](http://test.com)' + :tier | %w(gold premium) | 'gold, premium' + :distribution | %w(ce ee) | 'ce, ee' + :key_path | 'key.path' | '**key.path**' + :milestone | '13.4' | '13.4' + :status | 'data_available' | 'data_available' + end + + with_them do + subject { described_class.format(key, value) } + + it { is_expected.to eq(expected_value) } + end + end +end diff --git a/spec/lib/gitlab/usage/metric_definition_spec.rb b/spec/lib/gitlab/usage/metric_definition_spec.rb index e101f837324..bc64bdfbf56 100644 --- a/spec/lib/gitlab/usage/metric_definition_spec.rb +++ b/spec/lib/gitlab/usage/metric_definition_spec.rb @@ -5,17 +5,13 @@ require 'spec_helper' RSpec.describe Gitlab::Usage::MetricDefinition do let(:attributes) do { - name: 'uuid', description: 'GitLab instance unique identifier', value_type: 'string', product_category: 'collection', stage: 'growth', status: 'data_available', default_generation: 'generation_1', - full_path: { - generation_1: 'uuid', - generation_2: 'license.uuid' - }, + key_path: 'uuid', group: 'group::product analytics', time_frame: 'none', data_source: 'database', @@ -44,12 +40,11 @@ RSpec.describe Gitlab::Usage::MetricDefinition do using RSpec::Parameterized::TableSyntax where(:attribute, :value) do - :name | nil :description | nil :value_type | nil :value_type | 'test' :status | nil - :default_generation | nil + :key_path | nil :group | nil :time_frame | nil :time_frame | '29d' @@ -70,6 +65,20 @@ RSpec.describe Gitlab::Usage::MetricDefinition do described_class.new(path, attributes).validate! end + + context 'with skip_validation' do + it 'raise exception if skip_validation: false' do + expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).at_least(:once).with(instance_of(Gitlab::Usage::Metric::InvalidMetricError)) + + described_class.new(path, attributes.merge( { skip_validation: false } )).validate! + end + + it 'does not raise exception if has skip_validation: true' do + expect(Gitlab::ErrorTracking).not_to receive(:track_and_raise_for_dev_exception) + + described_class.new(path, attributes.merge( { skip_validation: true } )).validate! + end + end end end diff --git a/spec/lib/gitlab/usage/metric_spec.rb b/spec/lib/gitlab/usage/metric_spec.rb index 40671d980d6..d4a789419a4 100644 --- a/spec/lib/gitlab/usage/metric_spec.rb +++ b/spec/lib/gitlab/usage/metric_spec.rb @@ -4,15 +4,15 @@ require 'spec_helper' RSpec.describe Gitlab::Usage::Metric do describe '#definition' do - it 'returns generation_1 metric definiton' do - expect(described_class.new(default_generation_path: 'uuid').definition).to be_an(Gitlab::Usage::MetricDefinition) + it 'returns key_path metric definiton' do + expect(described_class.new(key_path: 'uuid').definition).to be_an(Gitlab::Usage::MetricDefinition) end end describe '#unflatten_default_path' do using RSpec::Parameterized::TableSyntax - where(:default_generation_path, :value, :expected_hash) do + where(:key_path, :value, :expected_hash) do 'uuid' | nil | { uuid: nil } 'uuid' | '1111' | { uuid: '1111' } 'counts.issues' | nil | { counts: { issues: nil } } @@ -21,7 +21,7 @@ RSpec.describe Gitlab::Usage::Metric do end with_them do - subject { described_class.new(default_generation_path: default_generation_path, value: value).unflatten_default_path } + subject { described_class.new(key_path: key_path, value: value).unflatten_key_path } it { is_expected.to eq(expected_hash) } end diff --git a/spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb b/spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb new file mode 100644 index 00000000000..a391872c030 --- /dev/null +++ b/spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb @@ -0,0 +1,256 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Usage::Metrics::Aggregates::Aggregate, :clean_gitlab_redis_shared_state do + let(:entity1) { 'dfb9d2d2-f56c-4c77-8aeb-6cddc4a1f857' } + let(:entity2) { '1dd9afb2-a3ee-4de1-8ae3-a405579c8584' } + let(:entity3) { '34rfjuuy-ce56-sa35-ds34-dfer567dfrf2' } + let(:entity4) { '8b9a2671-2abf-4bec-a682-22f6a8f7bf31' } + + around do |example| + # We need to freeze to a reference time + # because visits are grouped by the week number in the year + # Without freezing the time, the test may behave inconsistently + # depending on which day of the week test is run. + # Monday 6th of June + reference_time = Time.utc(2020, 6, 1) + travel_to(reference_time) { example.run } + end + + context 'aggregated_metrics_data' do + let(:known_events) do + [ + { name: 'event1_slot', redis_slot: "slot", category: 'category1', aggregation: "weekly" }, + { name: 'event2_slot', redis_slot: "slot", category: 'category2', aggregation: "weekly" }, + { name: 'event3_slot', redis_slot: "slot", category: 'category3', aggregation: "weekly" }, + { name: 'event5_slot', redis_slot: "slot", category: 'category4', aggregation: "weekly" }, + { name: 'event4', category: 'category2', aggregation: "weekly" } + ].map(&:with_indifferent_access) + end + + before do + allow(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:known_events).and_return(known_events) + end + + shared_examples 'aggregated_metrics_data' do + context 'no aggregated metrics is defined' do + it 'returns empty hash' do + allow_next_instance_of(described_class) do |instance| + allow(instance).to receive(:aggregated_metrics).and_return([]) + end + + expect(aggregated_metrics_data).to eq({}) + end + end + + context 'there are aggregated metrics defined' do + before do + allow_next_instance_of(described_class) do |instance| + allow(instance).to receive(:aggregated_metrics).and_return(aggregated_metrics) + end + end + + context 'with AND operator' do + let(:aggregated_metrics) do + [ + { name: 'gmau_1', events: %w[event1_slot event2_slot], operator: "AND" }, + { name: 'gmau_2', events: %w[event1_slot event2_slot event3_slot], operator: "AND" }, + { name: 'gmau_3', events: %w[event1_slot event2_slot event3_slot event5_slot], operator: "AND" }, + { name: 'gmau_4', events: %w[event4], operator: "AND" } + ].map(&:with_indifferent_access) + end + + it 'returns the number of unique events for all known events' do + results = { + 'gmau_1' => 3, + 'gmau_2' => 2, + 'gmau_3' => 1, + 'gmau_4' => 3 + } + + expect(aggregated_metrics_data).to eq(results) + end + end + + context 'with OR operator' do + let(:aggregated_metrics) do + [ + { name: 'gmau_1', events: %w[event3_slot event5_slot], operator: "OR" }, + { name: 'gmau_2', events: %w[event1_slot event2_slot event3_slot event5_slot], operator: "OR" }, + { name: 'gmau_3', events: %w[event4], operator: "OR" } + ].map(&:with_indifferent_access) + end + + it 'returns the number of unique events for all known events' do + results = { + 'gmau_1' => 2, + 'gmau_2' => 3, + 'gmau_3' => 3 + } + + expect(aggregated_metrics_data).to eq(results) + end + end + + context 'hidden behind feature flag' do + let(:enabled_feature_flag) { 'test_ff_enabled' } + let(:disabled_feature_flag) { 'test_ff_disabled' } + let(:aggregated_metrics) do + [ + # represents stable aggregated metrics that has been fully released + { name: 'gmau_without_ff', events: %w[event3_slot event5_slot], operator: "OR" }, + # represents new aggregated metric that is under performance testing on gitlab.com + { name: 'gmau_enabled', events: %w[event4], operator: "AND", feature_flag: enabled_feature_flag }, + # represents aggregated metric that is under development and shouldn't be yet collected even on gitlab.com + { name: 'gmau_disabled', events: %w[event4], operator: "AND", feature_flag: disabled_feature_flag } + ].map(&:with_indifferent_access) + end + + it 'returns the number of unique events for all known events' do + skip_feature_flags_yaml_validation + stub_feature_flags(enabled_feature_flag => true, disabled_feature_flag => false) + + expect(aggregated_metrics_data).to eq('gmau_without_ff' => 2, 'gmau_enabled' => 3) + end + end + end + + context 'error handling' do + context 'development and test environment' do + it 'raises error when unknown aggregation operator is used' do + allow_next_instance_of(described_class) do |instance| + allow(instance).to receive(:aggregated_metrics) + .and_return([{ name: 'gmau_1', events: %w[event1_slot], operator: "SUM" }]) + end + + expect { aggregated_metrics_data }.to raise_error Gitlab::Usage::Metrics::Aggregates::UnknownAggregationOperator + end + + it 're raises Gitlab::UsageDataCounters::HLLRedisCounter::EventError' do + error = Gitlab::UsageDataCounters::HLLRedisCounter::EventError + allow(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:calculate_events_union).and_raise(error) + + allow_next_instance_of(described_class) do |instance| + allow(instance).to receive(:aggregated_metrics) + .and_return([{ name: 'gmau_1', events: %w[event1_slot], operator: "OR" }]) + end + + expect { aggregated_metrics_data }.to raise_error error + end + end + + context 'production' do + before do + stub_rails_env('production') + end + + it 'rescues unknown aggregation operator error' do + allow_next_instance_of(described_class) do |instance| + allow(instance).to receive(:aggregated_metrics) + .and_return([{ name: 'gmau_1', events: %w[event1_slot], operator: "SUM" }]) + end + + expect(aggregated_metrics_data).to eq('gmau_1' => -1) + end + + it 'rescues Gitlab::UsageDataCounters::HLLRedisCounter::EventError' do + error = Gitlab::UsageDataCounters::HLLRedisCounter::EventError + allow(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:calculate_events_union).and_raise(error) + + allow_next_instance_of(described_class) do |instance| + allow(instance).to receive(:aggregated_metrics) + .and_return([{ name: 'gmau_1', events: %w[event1_slot], operator: "OR" }]) + end + + expect(aggregated_metrics_data).to eq('gmau_1' => -1) + end + end + end + end + + describe '.aggregated_metrics_weekly_data' do + subject(:aggregated_metrics_data) { described_class.new.weekly_data } + + before do + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event1_slot', values: entity1, time: 2.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event1_slot', values: entity2, time: 2.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event1_slot', values: entity3, time: 2.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event2_slot', values: entity1, time: 2.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event2_slot', values: entity2, time: 3.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event2_slot', values: entity3, time: 3.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event3_slot', values: entity1, time: 3.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event3_slot', values: entity2, time: 3.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event5_slot', values: entity2, time: 3.days.ago) + + # events out of time scope + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event2_slot', values: entity3, time: 8.days.ago) + + # events in different slots + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event4', values: entity1, time: 2.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event4', values: entity2, time: 2.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event4', values: entity4, time: 2.days.ago) + end + + it_behaves_like 'aggregated_metrics_data' + end + + describe '.aggregated_metrics_monthly_data' do + subject(:aggregated_metrics_data) { described_class.new.monthly_data } + + it_behaves_like 'aggregated_metrics_data' do + before do + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event1_slot', values: entity1, time: 2.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event1_slot', values: entity2, time: 2.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event1_slot', values: entity3, time: 2.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event2_slot', values: entity1, time: 2.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event2_slot', values: entity2, time: 3.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event2_slot', values: entity3, time: 3.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event3_slot', values: entity1, time: 3.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event3_slot', values: entity2, time: 10.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event5_slot', values: entity2, time: 4.weeks.ago.advance(days: 1)) + + # events out of time scope + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event5_slot', values: entity1, time: 4.weeks.ago.advance(days: -1)) + + # events in different slots + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event4', values: entity1, time: 2.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event4', values: entity2, time: 2.days.ago) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event('event4', values: entity4, time: 2.days.ago) + end + end + + context 'Redis calls' do + let(:aggregated_metrics) do + [ + { name: 'gmau_3', events: %w[event1_slot event2_slot event3_slot event5_slot], operator: "AND" } + ].map(&:with_indifferent_access) + end + + it 'caches intermediate operations' do + allow_next_instance_of(described_class) do |instance| + allow(instance).to receive(:aggregated_metrics).and_return(aggregated_metrics) + end + + aggregated_metrics[0][:events].each do |event| + expect(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:calculate_events_union) + .with(event_names: event, start_date: 4.weeks.ago.to_date, end_date: Date.current) + .once + .and_return(0) + end + + 2.upto(4) do |subset_size| + aggregated_metrics[0][:events].combination(subset_size).each do |events| + expect(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:calculate_events_union) + .with(event_names: events, start_date: 4.weeks.ago.to_date, end_date: Date.current) + .once + .and_return(0) + end + end + + aggregated_metrics_data + end + end + end + end +end |