Skip to content
代码片段 群组 项目
提交 08eb8612 编辑于 作者: Mikołaj Wawrzyniak's avatar Mikołaj Wawrzyniak 提交者: Niko Belokolodov
浏览文件

Add GIET generator

Add new rails generator that creates configuration files for
gitlab internal events tracking feature.
上级 e8cbd7fd
No related branches found
No related tags found
无相关合并请求
......@@ -44,6 +44,7 @@ ignore: |
#### Folders ####
node_modules/
tmp/
generator_templates/gitlab_internal_events/
# In CI some YAML files are linted using different rules.
# See `.gitlab/ci/yaml.gitlab-ci.yml`.
......
---
key_path: <%= args.second %>
name: <%= args.second %>
description: <%= args.last %>
product_section: <%= options.fetch(:section) %>
product_stage: <%= options.fetch(:stage) %>
product_group: <%= options.fetch(:group) %>
performance_indicator_type: []
value_type: number
status: active
milestone: "<%= milestone %>"
introduced_by_url: <%= options.fetch(:mr) %>
time_frame: <%= args.third %>
data_source: redis_hll
data_category: optional
instrumentation_class: <%= class_name %>
<%= distribution %>
<%= tier %>
# frozen_string_literal: true
require 'rails/generators'
module Gitlab
module Analytics
class InternalEventsGenerator < Rails::Generators::Base
TIME_FRAME_DIRS = {
'7d' => 'counts_7d',
'28d' => 'counts_28d'
}.freeze
TIME_FRAMES_DEFAULT = TIME_FRAME_DIRS.keys.tap do |time_frame_defaults|
time_frame_defaults.class_eval do
def to_s
join(", ")
end
end
end.freeze
ALLOWED_TIERS = %w[free premium ultimate].dup.tap do |tiers_default|
tiers_default.class_eval do
def to_s
join(", ")
end
end
end.freeze
TOP_LEVEL_DIR = 'config'
TOP_LEVEL_DIR_EE = 'ee'
DESCRIPTION_MIN_LENGTH = 50
KNOWN_EVENTS_PATH = 'lib/gitlab/usage_data_counters/known_events/common.yml'
KNOWN_EVENTS_PATH_EE = 'ee/lib/ee/gitlab/usage_data_counters/known_events/common.yml'
source_root File.expand_path('../../../../generator_templates/gitlab_internal_events', __dir__)
desc 'Generates metric definitions yml files and known events entries'
class_option :skip_namespace,
hide: true
class_option :skip_collision_check,
hide: true
class_option :time_frames,
optional: true,
default: TIME_FRAMES_DEFAULT,
type: :array,
banner: TIME_FRAMES_DEFAULT,
desc: "Indicates the metrics time frames. Please select one or more from: #{TIME_FRAMES_DEFAULT}"
class_option :tiers,
optional: true,
default: ALLOWED_TIERS,
type: :array,
banner: ALLOWED_TIERS,
desc: "Indicates the metric's GitLab subscription tiers. Please select one or more from: #{ALLOWED_TIERS}"
class_option :group,
type: :string,
optional: false,
desc: 'Name of group that added this metric'
class_option :stage,
type: :string,
optional: false,
desc: 'Name of stage that added this metric'
class_option :section,
type: :string,
optional: false,
desc: 'Name of section that added this metric'
class_option :mr,
type: :string,
optional: false,
desc: 'Merge Request that adds this metric'
class_option :event,
type: :string,
optional: false,
desc: 'Name of the event that this metric counts'
class_option :unique_on,
type: :string,
optional: false,
desc: 'Name of the event property that this metric counts'
def create_metric_file
validate!
time_frames.each do |time_frame|
template "metric_definition.yml",
file_path(time_frame),
key_path(time_frame),
time_frame,
ask_description(time_frame)
end
# ToDo: Delete during https://gitlab.com/groups/gitlab-org/-/epics/9542 cleanup
append_file known_events_file_name, known_event_entry
end
private
def known_event_entry
<<~YML
- name: #{options[:event]}
aggregation: weekly
YML
end
def ask_description(time_frame)
question = <<~DESC
Please describe in at least #{DESCRIPTION_MIN_LENGTH} characters
what #{key_path(time_frame)} metric represents,
consider mentioning: events, and event attributes in the description.
your answer will be processed to power a full-text search tool and help others find and reuse this metric.
DESC
say("")
description = ask(question)
while description.length < DESCRIPTION_MIN_LENGTH
error_mgs = <<~ERROR
Provided description is to short: #{description.length} of required #{DESCRIPTION_MIN_LENGTH} characters
ERROR
say(set_color(error_mgs), :red)
description = ask("Please provide description that is #{DESCRIPTION_MIN_LENGTH} characters long.\n")
end
description
end
def distribution
content = [
free? ? "- ce" : nil,
"- ee"
].compact.join("\n")
"distribution:\n#{content}"
end
def tier
"tier:\n- #{options[:tiers].join("\n- ")}"
end
def milestone
Gitlab::VERSION.match('(\d+\.\d+)').captures.first
end
def class_name
'RedisHLLMetric'
end
def key_path(time_frame)
"count_distinct_#{options[:unique_on]}_from_#{options[:event]}_#{time_frame}"
end
def file_path(time_frame)
path = File.join(TOP_LEVEL_DIR, 'metrics', TIME_FRAME_DIRS[time_frame], "#{key_path(time_frame)}.yml")
path = File.join(TOP_LEVEL_DIR_EE, path) unless free?
path
end
def known_events_file_name
(free? ? KNOWN_EVENTS_PATH : KNOWN_EVENTS_PATH_EE)
end
def validate!
raise "Required file: #{known_events_file_name} does not exists." unless File.exist?(known_events_file_name)
validate_tiers!
%i[unique_on event mr section stage group].each do |option|
raise "The option: --#{option} is missing" unless options.key? option
end
time_frames.each do |time_frame|
validate_time_frame!(time_frame)
validate_key_path!(time_frame)
end
end
def validate_time_frame!(time_frame)
return if TIME_FRAME_DIRS.key?(time_frame)
raise "Invalid time frame: #{time_frame}, allowed options are: #{TIME_FRAMES_DEFAULT}"
end
def validate_tiers!
wrong_tiers = options[:tiers] - ALLOWED_TIERS
unless wrong_tiers.empty?
raise "Tiers option included not allowed values: #{wrong_tiers}. Only allowed values are: #{ALLOWED_TIERS}"
end
return unless options[:tiers].empty?
raise "At least one tier must be present. Please set --tiers option"
end
def validate_key_path!(time_frame)
return unless metric_definition_exists?(time_frame)
raise "Metric definition with key path '#{key_path(time_frame)}' already exists"
end
def free?
options[:tiers].include? "free"
end
def time_frames
options[:time_frames]
end
def directory
@directory ||= TIME_FRAME_DIRS.find { |d| d.match?(input_dir) }
end
def metric_definitions
@definitions ||= Gitlab::Usage::MetricDefinition.definitions(skip_validation: true)
end
def metric_definition_exists?(time_frame)
metric_definitions[key_path(time_frame)].present?
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Analytics::InternalEventsGenerator, :silence_stdout, feature_category: :service_ping do
include UsageDataHelpers
let(:temp_dir) { Dir.mktmpdir }
let(:tmpfile) { Tempfile.new('test-metadata') }
let(:ee_temp_dir) { Dir.mktmpdir }
let(:existing_key_paths) { {} }
let(:description) { "This metric counts unique users viewing analytics metrics dashboard section" }
let(:group) { "group::analytics instrumentation" }
let(:stage) { "analytics" }
let(:section) { "analytics" }
let(:mr) { "https://gitlab.com/some-group/some-project/-/merge_requests/123" }
let(:event) { "view_analytics_dashboard" }
let(:unique_on) { "user_id" }
let(:options) do
{
time_frames: time_frames,
free: true,
mr: mr,
group: group,
stage: stage,
section: section,
event: event,
unique_on: unique_on
}.stringify_keys
end
let(:key_path_7d) { "count_distinct_#{unique_on}_from_#{event}_7d" }
let(:metric_definition_path_7d) { Dir.glob(File.join(temp_dir, "metrics/counts_7d/#{key_path_7d}.yml")).first }
let(:metric_definition_7d) do
{
"key_path" => key_path_7d,
"name" => key_path_7d,
"description" => description,
"product_section" => section,
"product_stage" => stage,
"product_group" => group,
"performance_indicator_type" => [],
"value_type" => "number",
"status" => "active",
"milestone" => "13.9",
"introduced_by_url" => mr,
"time_frame" => "7d",
"data_source" => "redis_hll",
"data_category" => "optional",
"instrumentation_class" => "RedisHLLMetric",
"distribution" => %w[ce ee],
"tier" => %w[free premium ultimate]
}
end
before do
stub_const("#{described_class}::TOP_LEVEL_DIR", temp_dir)
stub_const("#{described_class}::TOP_LEVEL_DIR_EE", ee_temp_dir)
stub_const("#{described_class}::KNOWN_EVENTS_PATH", tmpfile.path)
stub_const("#{described_class}::KNOWN_EVENTS_PATH_EE", tmpfile.path)
allow_next_instance_of(described_class) do |instance|
allow(instance).to receive(:ask)
.with(/Please describe in at least 50 characters/)
.and_return(description)
end
allow(Gitlab::Usage::MetricDefinition)
.to receive(:definitions).and_return(existing_key_paths)
end
after do
FileUtils.rm_rf(temp_dir)
FileUtils.rm_rf(ee_temp_dir)
FileUtils.rm_rf(tmpfile.path)
end
describe 'Creating metric definition file' do
before do
# Stub version so that `milestone` key remains constant between releases to prevent flakiness.
stub_const('Gitlab::VERSION', '13.9.0')
end
context 'for single time frame' do
let(:time_frames) { %w[7d] }
it 'creates a metric definition file using the template' do
described_class.new([], options).invoke_all
expect(YAML.safe_load(File.read(metric_definition_path_7d))).to eq(metric_definition_7d)
end
context 'for ultimate only feature' do
let(:metric_definition_path_7d) do
Dir.glob(File.join(ee_temp_dir, temp_dir, "metrics/counts_7d/#{key_path_7d}.yml")).first
end
it 'creates a metric definition file using the template' do
described_class.new([], options.merge(tiers: %w[ultimate])).invoke_all
expect(YAML.safe_load(File.read(metric_definition_path_7d)))
.to eq(metric_definition_7d.merge("tier" => ["ultimate"], "distribution" => ["ee"]))
end
end
context 'with invalid time frame' do
let(:time_frames) { %w[14d] }
it 'raises error' do
expect { described_class.new([], options).invoke_all }.to raise_error(RuntimeError)
end
end
context 'with duplicated key path' do
let(:existing_key_paths) { { key_path_7d => true } }
it 'raises error' do
expect { described_class.new([], options).invoke_all }.to raise_error(RuntimeError)
end
end
context 'without at least one tier available' do
it 'raises error' do
expect { described_class.new([], options.merge(tiers: [])).invoke_all }
.to raise_error(RuntimeError)
end
end
context 'with unknown tier' do
it 'raises error' do
expect { described_class.new([], options.merge(tiers: %w[superb])).invoke_all }
.to raise_error(RuntimeError)
end
end
context 'without obligatory parameter' do
it 'raises error', :aggregate_failures do
%w[unique_on event mr section stage group].each do |option|
expect { described_class.new([], options.without(option)).invoke_all }
.to raise_error(RuntimeError)
end
end
end
context 'with to short description' do
it 'asks again for description' do
allow_next_instance_of(described_class) do |instance|
allow(instance).to receive(:ask)
.with(/Please describe in at least 50 characters/)
.and_return("I am to short")
expect(instance).to receive(:ask)
.with(/Please provide description that is 50 characters long/)
.and_return(description)
end
described_class.new([], options).invoke_all
end
end
end
context 'for multiple time frames' do
let(:time_frames) { %w[7d 28d] }
let(:key_path_28d) { "count_distinct_#{unique_on}_from_#{event}_28d" }
let(:metric_definition_path_28d) { Dir.glob(File.join(temp_dir, "metrics/counts_28d/#{key_path_28d}.yml")).first }
let(:metric_definition_28d) do
metric_definition_7d.merge(
"key_path" => key_path_28d,
"name" => key_path_28d,
"time_frame" => "28d"
)
end
it 'creates a metric definition file using the template' do
described_class.new([], options).invoke_all
expect(YAML.safe_load(File.read(metric_definition_path_7d))).to eq(metric_definition_7d)
expect(YAML.safe_load(File.read(metric_definition_path_28d))).to eq(metric_definition_28d)
end
end
context 'with default time frames' do
let(:time_frames) { nil }
let(:key_path_28d) { "count_distinct_#{unique_on}_from_#{event}_28d" }
let(:metric_definition_path_28d) { Dir.glob(File.join(temp_dir, "metrics/counts_28d/#{key_path_28d}.yml")).first }
let(:metric_definition_28d) do
metric_definition_7d.merge(
"key_path" => key_path_28d,
"name" => key_path_28d,
"time_frame" => "28d"
)
end
it 'creates a metric definition file using the template' do
described_class.new([], options.without('time_frames')).invoke_all
expect(YAML.safe_load(File.read(metric_definition_path_7d))).to eq(metric_definition_7d)
expect(YAML.safe_load(File.read(metric_definition_path_28d))).to eq(metric_definition_28d)
end
end
end
describe 'Creating known event entry' do
let(:time_frames) { %w[7d 28d] }
let(:expected_known_events) { [{ "name" => event, "aggregation" => "weekly" }] }
it 'creates a metric definition file using the template' do
described_class.new([], options).invoke_all
expect(YAML.safe_load(File.read(tmpfile.path))).to match_array(expected_known_events)
end
context 'for ultimate only feature' do
let(:ee_tmpfile) { Tempfile.new('test-metadata') }
after do
FileUtils.rm_rf(ee_tmpfile)
end
it 'creates a metric definition file using the template' do
stub_const("#{described_class}::KNOWN_EVENTS_PATH_EE", ee_tmpfile.path)
described_class.new([], options.merge(tiers: %w[ultimate])).invoke_all
expect(YAML.safe_load(File.read(tmpfile.path))).to be nil
expect(YAML.safe_load(File.read(ee_tmpfile.path))).to match_array(expected_known_events)
end
end
end
end
0% 加载中 .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册