Module: Feature

Defined in:
lib/feature/shared.rb,
lib/feature.rb,
lib/feature/kas.rb,
lib/feature/gitaly.rb,
lib/feature/logger.rb,
lib/feature/definition.rb,
lib/feature/actor_wrapper.rb

Overview

This file can contain only simple constructs as it is shared between:

  1. ‘Pure Ruby`: bin/feature-flag

  2. ‘GitLab Rails`: lib/feature/definition.rb

Defined Under Namespace

Modules: Shared Classes: ActiveSupportCacheStoreAdapter, ActorWrapper, Definition, FlipperFeature, FlipperGate, FlipperGitlabInstance, FlipperPod, FlipperRecord, FlipperRequest, Gitaly, Kas, Logger, OptOut, Target

Constant Summary collapse

SUPPORTED_MODELS =
%w[
  User
  Project
  Namespace
  Ci::Runner
  Organizations::Organization
].freeze
InvalidFeatureFlagError =

rubocop:disable Lint/InheritException

Class.new(Exception)
InvalidOperation =
Class.new(ArgumentError)
RecursionError =
Class.new(RuntimeError)

Class Method Summary collapse

Class Method Details

.allObject



85
86
87
# File 'lib/feature.rb', line 85

def all
  flipper.features.to_a
end

.current_podObject



275
276
277
# File 'lib/feature.rb', line 275

def current_pod
  @flipper_pod ||= FlipperPod.new
end

.current_requestObject



267
268
269
270
271
272
273
# File 'lib/feature.rb', line 267

def current_request
  if Gitlab::SafeRequestStore.active?
    Gitlab::SafeRequestStore[:flipper_request] ||= FlipperRequest.new
  else
    @flipper_request ||= FlipperRequest.new
  end
end

.disable(key, thing = false) ⇒ Object



167
168
169
170
171
172
173
# File 'lib/feature.rb', line 167

def disable(key, thing = false)
  thing = sanitized_thing(thing)

  log(key: key, action: __method__, thing: thing)

  with_feature(key) { _1.disable(thing) }
end

.disable_percentage_of_actors(key) ⇒ Object



232
233
234
235
# File 'lib/feature.rb', line 232

def disable_percentage_of_actors(key)
  log(key: key, action: __method__)
  with_feature(key, &:disable_percentage_of_actors)
end

.disable_percentage_of_time(key) ⇒ Object



218
219
220
221
# File 'lib/feature.rb', line 218

def disable_percentage_of_time(key)
  log(key: key, action: __method__)
  with_feature(key, &:disable_percentage_of_time)
end

.disabled?(key, thing = nil, type: nil, default_enabled_if_undefined: nil) ⇒ Boolean

Returns:

  • (Boolean)


145
146
147
148
149
150
# File 'lib/feature.rb', line 145

def disabled?(key, thing = nil, type: nil, default_enabled_if_undefined: nil)
  thing = sanitized_thing(thing)

  # we need to make different method calls to make it easy to mock / define expectations in test mode
  thing.nil? ? !enabled?(key, type: type, default_enabled_if_undefined: default_enabled_if_undefined) : !enabled?(key, thing, type: type, default_enabled_if_undefined: default_enabled_if_undefined)
end

.enable(key, thing = true) ⇒ Object



152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/feature.rb', line 152

def enable(key, thing = true)
  thing = sanitized_thing(thing)

  log(key: key, action: __method__, thing: thing)

  return_value = with_feature(key) { _1.enable(thing) }

  # rubocop:disable Gitlab/RailsLogger
  Rails.logger.warn('WARNING: Understand the stability and security risks of enabling in-development features with feature flags.')
  Rails.logger.warn('See https://docs.gitlab.com/ee/administration/feature_flags.html#risks-when-enabling-features-still-in-development for more information.')
  # rubocop:enable Gitlab/RailsLogger

  return_value
end

.enable_percentage_of_actors(key, percentage) ⇒ Object



223
224
225
226
227
228
229
230
# File 'lib/feature.rb', line 223

def enable_percentage_of_actors(key, percentage)
  log(key: key, action: __method__, percentage: percentage)
  with_feature(key) do |flag|
    raise InvalidOperation, 'Cannot enable percentage of actors for a fully-enabled flag' if flag.state == :on

    flag.enable_percentage_of_actors(percentage)
  end
end

.enable_percentage_of_time(key, percentage) ⇒ Object



209
210
211
212
213
214
215
216
# File 'lib/feature.rb', line 209

def enable_percentage_of_time(key, percentage)
  log(key: key, action: __method__, percentage: percentage)
  with_feature(key) do |flag|
    raise InvalidOperation, 'Cannot enable percentage of time for a fully-enabled flag' if flag.state == :on

    flag.enable_percentage_of_time(percentage)
  end
end

.enabled?(key, thing = nil, type: nil, default_enabled_if_undefined: nil) ⇒ Boolean

The default state of feature flag is read from YAML:

  1. If feature flag does not have YAML it will fallback to ‘default_enabled: false` in production environment, but raise exception in development or tests.

  2. The default_enabled_if_undefined: is tech debt related to Gitaly flags and should not be used outside of Gitaly’s lib/feature/gitaly.rb

Returns:

  • (Boolean)


128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/feature.rb', line 128

def enabled?(key, thing = nil, type: nil, default_enabled_if_undefined: nil)
  thing = sanitized_thing(thing)

  check_feature_flags_definition!(key, thing, type)

  default_enabled = Feature::Definition.default_enabled?(key, default_enabled_if_undefined: default_enabled_if_undefined)
  feature_value = current_feature_value(key, thing, default_enabled: default_enabled)

  # If not yielded, then either recursion is happening, or the database does not exist yet, so use default_enabled.
  feature_value = default_enabled if feature_value.nil?

  # If we don't filter out this flag here we will enter an infinite loop
  log_feature_flag_state(key, feature_value) if log_feature_flag_states?(key)

  feature_value
end

.get(key) ⇒ Object



100
101
102
# File 'lib/feature.rb', line 100

def get(key)
  with_feature(key, &:itself)
end

.gitlab_instanceObject



279
280
281
# File 'lib/feature.rb', line 279

def gitlab_instance
  @flipper_gitlab_instance ||= FlipperGitlabInstance.new
end

.group_ids_for(feature_key) ⇒ Object

rubocop: disable CodeReuse/ActiveRecord – rubocop doesn’t recognize Flipper::Adapters::ActiveRecord::Gate as ActiveRecord.



300
301
302
303
304
305
# File 'lib/feature.rb', line 300

def group_ids_for(feature_key)
  FlipperGate.where(feature_key: feature_key)
             .pluck(:value)
             .select { |v| v.start_with?("Group:") }
             .map { |v| v.sub("Group:", "") }
end

.log_feature_flag_state(key, feature_value) ⇒ Object



291
292
293
# File 'lib/feature.rb', line 291

def log_feature_flag_state(key, feature_value)
  logged_states[key] ||= feature_value
end

.log_feature_flag_states?(key) ⇒ Boolean

Returns:

  • (Boolean)


287
288
289
# File 'lib/feature.rb', line 287

def log_feature_flag_states?(key)
  Feature::Definition.log_states?(key)
end

.logged_statesObject



295
296
297
# File 'lib/feature.rb', line 295

def logged_states
  RequestStore.fetch(:feature_flag_events) { {} }
end

.loggerObject



283
284
285
# File 'lib/feature.rb', line 283

def logger
  @logger ||= Feature::Logger.build
end

.opt_out(key, thing) ⇒ Object



186
187
188
189
190
191
192
193
194
195
# File 'lib/feature.rb', line 186

def opt_out(key, thing)
  thing = sanitized_thing(thing)

  return unless thing.respond_to?(:flipper_id) # Ignore Feature::Types::Group

  log(key: key, action: __method__, thing: thing)
  opt_out = OptOut.new(thing)

  with_feature(key) { _1.enable(opt_out) }
end

.opted_out?(key, thing) ⇒ Boolean

Returns:

  • (Boolean)


175
176
177
178
179
180
181
182
183
184
# File 'lib/feature.rb', line 175

def opted_out?(key, thing)
  thing = sanitized_thing(thing)

  return false unless thing.respond_to?(:flipper_id) # Ignore Feature::Types::Group
  return false unless persisted_name?(key)

  opt_out = OptOut.new(thing)

  with_feature(key) { _1.actors_value.include?(opt_out.flipper_id) }
end

.persisted_name?(feature_name) ⇒ Boolean

Returns:

  • (Boolean)


116
117
118
119
120
121
# File 'lib/feature.rb', line 116

def persisted_name?(feature_name)
  # Flipper creates on-memory features when asked for a not-yet-created one.
  # If we want to check if a feature has been actually set, we look for it
  # on the persisted features list.
  persisted_names.include?(feature_name.to_s)
end

.persisted_namesObject



104
105
106
107
108
109
110
111
112
113
114
# File 'lib/feature.rb', line 104

def persisted_names
  return [] unless database_exists?

  # This loads names of all stored feature flags
  # and returns a stable Set in the following order:
  # - Memoized: using Gitlab::SafeRequestStore or @flipper
  # - L1: using Process cache
  # - L2: using Redis cache
  # - DB: using a single SQL query
  flipper.adapter.features
end

.preload(names) ⇒ Object

Preload the features with the given names.

names - An Array of String or Symbol names of the features.

github.com/flippercloud/flipper/blob/bf6a13f34fc7f45a597c3d66ec291f3e5855e830/lib/flipper/dsl.rb#L229



94
95
96
# File 'lib/feature.rb', line 94

def preload(names)
  flipper.preload(names) # rubocop:disable CodeReuse/ActiveRecord -- This cop is not relevant in the Flipper context
end

.register_definitionsObject



257
258
259
# File 'lib/feature.rb', line 257

def register_definitions
  Feature::Definition.reload!
end

.register_feature_groupsObject

This method is called from config/initializers/0_inject_feature_flags.rb and can be used to register Flipper groups. See docs.gitlab.com/ee/development/feature_flags/

EE feature groups should go inside the ee/lib/ee/feature.rb version of this method.



255
# File 'lib/feature.rb', line 255

def register_feature_groups; end

.register_hot_reloaderObject



261
262
263
264
265
# File 'lib/feature.rb', line 261

def register_hot_reloader
  return unless check_feature_flags_definition?

  Feature::Definition.register_hot_reloader!
end

.remove(key) ⇒ Object



237
238
239
240
241
242
243
# File 'lib/feature.rb', line 237

def remove(key)
  return unless persisted_name?(key)

  log(key: key, action: __method__)

  with_feature(key, &:remove)
end

.remove_opt_out(key, thing) ⇒ Object



197
198
199
200
201
202
203
204
205
206
207
# File 'lib/feature.rb', line 197

def remove_opt_out(key, thing)
  thing = sanitized_thing(thing)

  return unless thing.respond_to?(:flipper_id) # Ignore Feature::Types::Group
  return unless persisted_name?(key)

  log(key: key, action: __method__, thing: thing)
  opt_out = OptOut.new(thing)

  with_feature(key) { _1.disable(opt_out) }
end

.resetObject



245
246
247
248
# File 'lib/feature.rb', line 245

def reset
  Gitlab::SafeRequestStore.delete(:flipper) if Gitlab::SafeRequestStore.active?
  @flipper = nil
end