summaryrefslogtreecommitdiff
path: root/spec/lib/gitlab/usage/service_ping_report_spec.rb
blob: 1f62ddd0bbbd459f8e666ec4d39f523ba66e14cb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Gitlab::Usage::ServicePingReport, :use_clean_rails_memory_store_caching do
  include UsageDataHelpers

  let(:usage_data) { { uuid: "1111", counts: { issue: 0 } } }

  context 'when feature merge_service_ping_instrumented_metrics enabled' do
    before do
      stub_feature_flags(merge_service_ping_instrumented_metrics: true)

      allow_next_instance_of(Gitlab::Usage::ServicePing::PayloadKeysProcessor) do |instance|
        allow(instance).to receive(:missing_key_paths).and_return([])
      end

      allow_next_instance_of(Gitlab::Usage::ServicePing::InstrumentedPayload) do |instance|
        allow(instance).to receive(:build).and_return({})
      end
    end

    context 'all_metrics_values' do
      it 'generates the service ping when there are no missing values' do
        expect(Gitlab::UsageData).to receive(:data).and_return(usage_data)
        expect(described_class.for(output: :all_metrics_values)).to eq({ uuid: "1111", counts: { issue: 0 } })
      end

      it 'generates the service ping with the missing values' do
        expect_next_instance_of(Gitlab::Usage::ServicePing::PayloadKeysProcessor, usage_data) do |instance|
          expect(instance).to receive(:missing_instrumented_metrics_key_paths).and_return(['counts.boards'])
        end

        expect_next_instance_of(Gitlab::Usage::ServicePing::InstrumentedPayload, ['counts.boards'], :with_value) do |instance|
          expect(instance).to receive(:build).and_return({ counts: { boards: 1 } })
        end

        expect(Gitlab::UsageData).to receive(:data).and_return(usage_data)
        expect(described_class.for(output: :all_metrics_values)).to eq({ uuid: "1111", counts: { issue: 0, boards: 1 } })
      end
    end

    context 'for output: :metrics_queries' do
      it 'generates the service ping' do
        expect(Gitlab::UsageData).to receive(:data).and_return(usage_data)

        described_class.for(output: :metrics_queries)
      end
    end

    context 'for output: :non_sql_metrics_values' do
      it 'generates the service ping' do
        expect(Gitlab::UsageData).to receive(:data).and_return(usage_data)

        described_class.for(output: :non_sql_metrics_values)
      end
    end

    context 'when using cached' do
      context 'for cached: true' do
        let(:new_usage_data) { { uuid: "1112" } }

        it 'caches the values' do
          allow(Gitlab::UsageData).to receive(:data).and_return(usage_data, new_usage_data)

          expect(described_class.for(output: :all_metrics_values)).to eq(usage_data)
          expect(described_class.for(output: :all_metrics_values, cached: true)).to eq(usage_data)

          expect(Rails.cache.fetch('usage_data')).to eq(usage_data)
        end

        it 'writes to cache and returns fresh data' do
          allow(Gitlab::UsageData).to receive(:data).and_return(usage_data, new_usage_data)

          expect(described_class.for(output: :all_metrics_values)).to eq(usage_data)
          expect(described_class.for(output: :all_metrics_values)).to eq(new_usage_data)
          expect(described_class.for(output: :all_metrics_values, cached: true)).to eq(new_usage_data)

          expect(Rails.cache.fetch('usage_data')).to eq(new_usage_data)
        end
      end

      context 'when no caching' do
        let(:new_usage_data) { { uuid: "1112" } }

        it 'returns fresh data' do
          allow(Gitlab::UsageData).to receive(:data).and_return(usage_data, new_usage_data)

          expect(described_class.for(output: :all_metrics_values)).to eq(usage_data)
          expect(described_class.for(output: :all_metrics_values)).to eq(new_usage_data)

          expect(Rails.cache.fetch('usage_data')).to eq(new_usage_data)
        end
      end
    end
  end

  context 'when feature merge_service_ping_instrumented_metrics disabled' do
    before do
      stub_feature_flags(merge_service_ping_instrumented_metrics: false)
    end

    context 'all_metrics_values' do
      it 'generates the service ping when there are no missing values' do
        expect(Gitlab::UsageData).to receive(:data).and_return(usage_data)
        expect(described_class.for(output: :all_metrics_values)).to eq({ uuid: "1111", counts: { issue: 0 } })
      end
    end

    context 'for output: :metrics_queries' do
      it 'generates the service ping' do
        expect(Gitlab::UsageData).to receive(:data).and_return(usage_data)

        described_class.for(output: :metrics_queries)
      end
    end

    context 'for output: :non_sql_metrics_values' do
      it 'generates the service ping' do
        expect(Gitlab::UsageData).to receive(:data).and_return(usage_data)

        described_class.for(output: :non_sql_metrics_values)
      end
    end
  end

  context 'cross test values against queries' do
    # TODO: fix failing metrics https://gitlab.com/gitlab-org/gitlab/-/issues/353559
    let(:failing_todo_metrics) do
      ["counts.labels",
       "counts.jira_imports_total_imported_issues_count",
       "counts.in_product_marketing_email_create_0_sent",
       "counts.in_product_marketing_email_create_0_cta_clicked",
       "counts.in_product_marketing_email_create_1_sent",
       "counts.in_product_marketing_email_create_1_cta_clicked",
       "counts.in_product_marketing_email_create_2_sent",
       "counts.in_product_marketing_email_create_2_cta_clicked",
       "counts.in_product_marketing_email_verify_0_sent",
       "counts.in_product_marketing_email_verify_0_cta_clicked",
       "counts.in_product_marketing_email_verify_1_sent",
       "counts.in_product_marketing_email_verify_1_cta_clicked",
       "counts.in_product_marketing_email_verify_2_sent",
       "counts.in_product_marketing_email_verify_2_cta_clicked",
       "counts.in_product_marketing_email_trial_0_sent",
       "counts.in_product_marketing_email_trial_0_cta_clicked",
       "counts.in_product_marketing_email_trial_1_sent",
       "counts.in_product_marketing_email_trial_1_cta_clicked",
       "counts.in_product_marketing_email_trial_2_sent",
       "counts.in_product_marketing_email_trial_2_cta_clicked",
       "counts.in_product_marketing_email_team_0_sent",
       "counts.in_product_marketing_email_team_0_cta_clicked",
       "counts.in_product_marketing_email_team_1_sent",
       "counts.in_product_marketing_email_team_1_cta_clicked",
       "counts.in_product_marketing_email_team_2_sent",
       "counts.in_product_marketing_email_team_2_cta_clicked",
       "counts.in_product_marketing_email_experience_0_sent",
       "counts.in_product_marketing_email_team_short_0_sent",
       "counts.in_product_marketing_email_team_short_0_cta_clicked",
       "counts.in_product_marketing_email_trial_short_0_sent",
       "counts.in_product_marketing_email_trial_short_0_cta_clicked",
       "counts.in_product_marketing_email_admin_verify_0_sent",
       "counts.in_product_marketing_email_admin_verify_0_cta_clicked",
       "counts.ldap_users",
       "usage_activity_by_stage.create.projects_with_sectional_code_owner_rules",
       "usage_activity_by_stage.monitor.clusters_integrations_prometheus",
       "usage_activity_by_stage.monitor.projects_with_enabled_alert_integrations_histogram",
       "usage_activity_by_stage_monthly.create.projects_with_sectional_code_owner_rules",
       "usage_activity_by_stage_monthly.monitor.clusters_integrations_prometheus"]
    end

    def fetch_value_by_query(query)
      # Because test cases are run inside a transaction, if any query raise and error all queries that follows
      # it are automatically canceled by PostgreSQL, to avoid that problem, and to provide exhaustive information
      # about every metric, queries are wrapped explicitly in sub transactions.
      ApplicationRecord.transaction do
        ApplicationRecord.connection.execute(query)&.first&.values&.first
      end
    rescue ActiveRecord::StatementInvalid => e
      e.message
    end

    def build_payload_from_queries(payload, accumulator = [], key_path = [])
      payload.each do |key, value|
        if value.is_a?(Hash)
          build_payload_from_queries(value, accumulator, key_path.dup << key)
        elsif value.is_a?(String) && /SELECT .* FROM.*/ =~ value
          accumulator << [key_path.dup << key, value, fetch_value_by_query(value)]
        end
      end
      accumulator
    end

    before do
      stub_usage_data_connections
      stub_object_store_settings
      stub_prometheus_queries
      memoized_constatns = Gitlab::UsageData::CE_MEMOIZED_VALUES
      memoized_constatns += Gitlab::UsageData::EE_MEMOIZED_VALUES if defined? Gitlab::UsageData::EE_MEMOIZED_VALUES
      memoized_constatns.each { |v| Gitlab::UsageData.clear_memoization(v) }
      stub_database_flavor_check('Cloud SQL for PostgreSQL')
    end

    let(:service_ping_payload) { described_class.for(output: :all_metrics_values) }
    let(:metrics_queries_with_values) { build_payload_from_queries(described_class.for(output: :metrics_queries)) }

    it 'generates queries that match collected data', :aggregate_failures do
      message = "Expected %{query} result to match %{value} for %{key_path} metric"

      metrics_queries_with_values.each do |key_path, query, value|
        next if failing_todo_metrics.include?(key_path.join('.'))

        expect(value).to(
          eq(service_ping_payload.dig(*key_path)),
          message % { query: query, value: (value || 'NULL'), key_path: key_path.join('.') }
        )
      end
    end
  end
end