summaryrefslogtreecommitdiff
path: root/spec/lib/gitlab/usage
diff options
context:
space:
mode:
Diffstat (limited to 'spec/lib/gitlab/usage')
-rw-r--r--spec/lib/gitlab/usage/docs/renderer_spec.rb21
-rw-r--r--spec/lib/gitlab/usage/docs/value_formatter_spec.rb26
-rw-r--r--spec/lib/gitlab/usage/metric_definition_spec.rb23
-rw-r--r--spec/lib/gitlab/usage/metric_spec.rb8
-rw-r--r--spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb256
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