File: metric_options.rb

package info (click to toggle)
gitlab 17.6.5-19
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 629,368 kB
  • sloc: ruby: 1,915,304; javascript: 557,307; sql: 60,639; xml: 6,509; sh: 4,567; makefile: 1,239; python: 406
file content (244 lines) | stat: -rwxr-xr-x 10,493 bytes parent folder | download
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
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# frozen_string_literal: true

# Helpers related to listing existing metric definitions
module InternalEventsCli
  module Helpers
    module MetricOptions
      # Creates a list of metrics to be used as options in a
      # select/multiselect menu; existing metrics and metrics for
      # unavailable identifiers are marked as disabled
      #
      # @param events [Array<ExistingEvent>]
      # @return [Array<Hash>] hash (compact) has keys/values:
      #   value: [Array<NewMetric>]
      #   name: [String] formatted description of the metrics
      #   disabled: [String] reason metrics are disabled
      def get_metric_options(events)
        selection = EventSelection.new(events)

        options = get_all_metric_options(selection.actions)
        options = options.group_by do |metric|
          [
            metric.identifier.value,
            conflicting_metric_exists?(metric),
            metric.filters_expected?,
            metric.time_frame.value == 'all'
          ]
        end

        options = options.filter_map do |(identifier, defined, filtered, _), metrics|
          # Hide the filtered version of an option if unsupported; it just adds noise without value. Still,
          # showing unsupported options is valuable, because it advertises possibilities and explains why
          # those options aren't available.
          next if filtered && !selection.can_be_unique?(identifier)
          next if filtered && !selection.can_filter_when_unique?(identifier)
          next if selection.exclude_filter_identifier?(identifier)

          Option.new(
            identifier: identifier,
            events_name: selection.events_name,
            filter_name: (selection.filter_name(identifier) if filtered),
            metrics: metrics,
            defined: defined,
            supported: selection.can_be_unique?(identifier)
          ).formatted
        end

        # Push disabled options to the end for better skimability;
        # retain relative order for continuity
        options.partition { |opt| !opt[:disabled] }.flatten
      end

      private

      # Lists all potential metrics supported in service ping,
      # ordered by: identifier > filters > time_frame
      #
      # @param actions [Array<String>] event names
      # @return [Array<NewMetric>]
      def get_all_metric_options(actions)
        [
          Metric.new(actions: actions, time_frame: '28d', identifier: 'user'),
          Metric.new(actions: actions, time_frame: '7d', identifier: 'user'),
          Metric.new(actions: actions, time_frame: '28d', identifier: 'project'),
          Metric.new(actions: actions, time_frame: '7d', identifier: 'project'),
          Metric.new(actions: actions, time_frame: '28d', identifier: 'namespace'),
          Metric.new(actions: actions, time_frame: '7d', identifier: 'namespace'),
          Metric.new(actions: actions, time_frame: '28d', identifier: 'user', filters: []),
          Metric.new(actions: actions, time_frame: '7d', identifier: 'user', filters: []),
          Metric.new(actions: actions, time_frame: '28d', identifier: 'project', filters: []),
          Metric.new(actions: actions, time_frame: '7d', identifier: 'project', filters: []),
          Metric.new(actions: actions, time_frame: '28d', identifier: 'namespace', filters: []),
          Metric.new(actions: actions, time_frame: '7d', identifier: 'namespace', filters: []),
          Metric.new(actions: actions, time_frame: '28d'),
          Metric.new(actions: actions, time_frame: '7d'),
          Metric.new(actions: actions, time_frame: '28d', filters: []),
          Metric.new(actions: actions, time_frame: '7d', filters: []),
          Metric.new(actions: actions, time_frame: 'all'),
          Metric.new(actions: actions, time_frame: 'all', filters: []),
          Metric.new(actions: actions, time_frame: '28d', identifier: 'label'),
          Metric.new(actions: actions, time_frame: '7d', identifier: 'label'),
          Metric.new(actions: actions, time_frame: '28d', identifier: 'property'),
          Metric.new(actions: actions, time_frame: '7d', identifier: 'property'),
          Metric.new(actions: actions, time_frame: '28d', identifier: 'value'),
          Metric.new(actions: actions, time_frame: '7d', identifier: 'value'),
          Metric.new(actions: actions, time_frame: '28d', identifier: 'label', filters: []),
          Metric.new(actions: actions, time_frame: '7d', identifier: 'label', filters: []),
          Metric.new(actions: actions, time_frame: '28d', identifier: 'property', filters: []),
          Metric.new(actions: actions, time_frame: '7d', identifier: 'property', filters: []),
          Metric.new(actions: actions, time_frame: '28d', identifier: 'value', filters: []),
          Metric.new(actions: actions, time_frame: '7d', identifier: 'value', filters: [])
        ]
      end

      # Checks if there's an existing metric which has the same
      # properties as the new one
      #
      # @param new_metric [NewMetric]
      # @return [Boolean]
      def conflicting_metric_exists?(new_metric)
        # metrics with filters are conflict-free until new filters are defined
        return false if new_metric.filters_expected?

        cli.global.metrics.any? do |existing_metric|
          existing_metric.actions == new_metric.actions &&
            existing_metric.time_frame == new_metric.time_frame.value &&
            existing_metric.identifier == new_metric.identifier.value &&
            !existing_metric.filtered?
        end
      end

      # Represents the attributes of set of events that depend on
      # the other events in the set
      EventSelection = Struct.new(:events) do
        def actions
          events.map(&:action)
        end

        # Very brief summary of the provided events to use in a
        # basic description of the metric
        # This ignores filters for simplicity & skimability
        def events_name
          return actions.first if actions.length == 1

          "any of #{actions.length} events"
        end

        # Formatted list of filter options for these events, given
        # the provided uniqueness constraint
        def filter_name(identifier)
          filter_options.difference([identifier]).join('/')
        end

        # We accept different filters for each event, so we want
        # any filter options available for any event
        def filter_options
          events.flat_map(&:available_filters).uniq
        end

        # We require the same uniqueness constraint for all events,
        # so we want only the options they have in common
        def uniqueness_options
          [*shared_identifiers, *shared_filters, nil]
        end

        # Whether there are any filtering options other than the
        # selected uniqueness constraint
        def can_filter_when_unique?(identifier)
          can_be_unique?(identifier) && filter_options.difference([identifier]).any?
        end

        # Whether the given identifier is available for all events
        # and can be used as a uniqueness constraint
        def can_be_unique?(identifier)
          uniqueness_options.include?(identifier)
        end

        # Common values for identifiers shared across all the events
        def shared_identifiers
          events.map(&:identifiers).reduce(&:&)
        end

        # Common values for filters shared across all the events
        def shared_filters
          events.map(&:available_filters).reduce(&:&)
        end

        # Whether none of the events have additional properties
        # and the given identifier is an additional property.
        # In this case, it makes sense to exclude these from the
        # menu to keep the flow simple when the use-case is simple
        def exclude_filter_identifier?(identifier)
          return false if identifier.nil? || Metric::Identifier.new(identifier).default?

          filter_options.empty?
        end
      end

      # Formats & structures a single select/multiselect menu item
      #
      # @param identifier [String, nil] if present, used in unique-by-identifier metrics
      # @param events_name [String] how the selected events will be referred to as a group
      # @param filter_name [String] how the potential filters will be referred to as a group
      # @param metrics [Array<NewMetric>]
      # @option defined [Boolean] whether this metric already exists
      # @option supported [Boolean] whether unique metrics are supported for this identifier
      Option = Struct.new(:identifier, :events_name, :filter_name, :metrics, :defined, :supported,
        keyword_init: true) do
        include InternalEventsCli::Helpers::Formatting

        # @return [Hash] see #get_metric_options for format
        # ex) Monthly/Weekly count of unique users who triggered cli_template_included where label/property is...
        # ex) Monthly/Weekly count of unique users who triggered cli_template_included (user unavailable)
        def formatted
          name = [time_frame_phrase, identifier_phrase, filter_phrase].compact.join(' ')
          name = format_help(name) if disabled

          { name: name, disabled: disabled, value: metrics }.compact
        end

        def identifier
          Metric::Identifier.new(self[:identifier])
        end

        # ex) "Monthly/Weekly"
        def time_frame_phrase
          phrase = metrics.map { |metric| metric.time_frame.description }.join('/')

          disabled ? phrase : format_info(phrase)
        end

        # ex) "count of unique users who triggered cli_template_included"
        def identifier_phrase
          phrase = identifier.description % events_name
          phrase.gsub!(unique_phrase, format_info(unique_phrase)) unless disabled

          phrase
        end

        # ex) "unique users"
        def unique_phrase
          "unique #{identifier.plural}"
        end

        # ex) "where label/property is..."
        def filter_phrase
          return unless filter_name
          return "where filtered" if disabled

          "#{format_info("where #{filter_name}")} is..."
        end

        # Returns the string to include at the end of disabled
        # menu items. Nil if menu item shouldn't be disabled
        def disabled
          if defined
            pastel.bold(format_help("(already defined)"))
          elsif !supported
            pastel.bold(format_help("(#{identifier.value} unavailable)"))
          end
        end
      end
    end
  end
end