gitlab-ce/lib/generators/gitlab/analytics/internal_events_generator.rb

277 lines
8.1 KiB
Ruby

# frozen_string_literal: true
require 'rails/generators'
module Gitlab
module Analytics
class InternalEventsGenerator < Rails::Generators::Base
TIME_FRAME_DIRS = {
'all' => 'counts_all',
'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
NEGATIVE_ANSWERS = %w[no n No NO N].freeze
POSITIVE_ANSWERS = %w[yes y Yes YES Y].freeze
TOP_LEVEL_DIR = 'config'
TOP_LEVEL_DIR_EE = 'ee'
DESCRIPTION_MIN_LENGTH = 50
DESCRIPTION_INQUIRY = %(
Please describe in at least #{DESCRIPTION_MIN_LENGTH} characters
what %{entity} %{entity_type} represents,
consider mentioning: %{considerations}.
Your answer will be processed by a full-text search tool and help others find and reuse this %{entity_type}.
).freeze
source_root File.expand_path('../../../../generator_templates/gitlab_internal_events', __dir__)
desc 'Generates metric definitions and event definition yml files'
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,
type: :string,
optional: true,
desc: 'Name of the event property that this metric counts'
def create_metric_file
validate!
unless event_exists?
template "event_definition.yml",
event_file_path(event),
ask_description(event, "event", "what the event is supposed to track, where, and when")
end
time_frames.each do |time_frame|
template "metric_definition.yml",
metric_file_path(time_frame),
key_path(time_frame),
time_frame,
ask_description(
key_path(time_frame),
"metric",
"events, and event attributes in the description"
)
end
end
private
def event_identifiers
return unless include_default_event_properties?
"\n- project\n- user\n- namespace"
end
def include_default_event_properties?
question = <<~DESC
By convention all events automatically include the following properties:
* environment: string,
* source: string (eg: ruby, javascript)
* user_id: number
* project_id: number
* namespace_id: number
* plan: string (eg: free, premium, ultimate)
Would you like to add default properties to the event? Y(es)/N(o)
DESC
answer = Gitlab::TaskHelpers.prompt(question, POSITIVE_ANSWERS + NEGATIVE_ANSWERS)
POSITIVE_ANSWERS.include?(answer)
end
def event_file_path(event)
path = File.join(TOP_LEVEL_DIR, 'events', "#{event}.yml")
path = File.join(TOP_LEVEL_DIR_EE, path) unless free?
path
end
def event
options[:event]
end
def unique(time_frame)
return if time_frame == 'all'
"\n unique: #{options.fetch(:unique)}"
end
def ask_description(entity, type, considerations)
say("")
desc = ask(format(DESCRIPTION_INQUIRY, entity: entity, entity_type: type, considerations: considerations))
while desc.length < DESCRIPTION_MIN_LENGTH
error_msg = <<~ERROR
Provided description is too short: #{desc.length} of required #{DESCRIPTION_MIN_LENGTH} characters
ERROR
say(set_color(error_msg, :red))
desc = ask("Please provide description that is #{DESCRIPTION_MIN_LENGTH} characters long.\n")
end
desc
end
def distributions
dist = "\n"
dist += "- ce\n" if free?
"#{dist}- ee"
end
def tiers
"\n- #{options[:tiers].join("\n- ")}"
end
def milestone
Gitlab::VERSION.match('(\d+\.\d+)').captures.first
end
def class_name(time_frame)
time_frame == 'all' ? 'TotalCountMetric' : 'RedisHLLMetric'
end
def key_path(time_frame)
if time_frame == 'all'
"count_total_#{event}"
else
"count_distinct_#{options[:unique].sub('.', '_')}_from_#{event}_#{time_frame}"
end
end
def metric_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 validate!
validate_tiers!
%i[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)
raise "The option: --unique is missing" if time_frame != 'all' && !options.key?('unique')
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 event_exists?
return true if ::Gitlab::UsageDataCounters::HLLRedisCounter.known_event?(event)
existing_events_from_definitions.include?(event)
end
def existing_events_from_definitions
events_glob_path = File.join(TOP_LEVEL_DIR, 'events', "*.yml")
ee_events_glob_path = File.join(TOP_LEVEL_DIR_EE, events_glob_path)
[ee_events_glob_path, events_glob_path].flat_map do |glob_path|
Dir.glob(glob_path).map do |path|
YAML.safe_load(File.read(path))["action"]
end
end
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
end
def metric_definition_exists?(time_frame)
metric_definitions[key_path(time_frame)].present?
end
end
end
end