Module: Feature

Defined in:
lib/feature/shared.rb,
lib/feature.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, Logger, OptOut, Target

Constant Summary collapse

InvalidFeatureFlagError =

rubocop:disable Lint/InheritException

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

Class Method Summary collapse

Class Method Details

.allObject



76
77
78
# File 'lib/feature.rb', line 76

def all
  flipper.features.to_a
end

.current_podObject



266
267
268
# File 'lib/feature.rb', line 266

def current_pod
  @flipper_pod ||= FlipperPod.new
end

.current_requestObject



258
259
260
261
262
263
264
# File 'lib/feature.rb', line 258

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



158
159
160
161
162
163
164
# File 'lib/feature.rb', line 158

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



223
224
225
226
# File 'lib/feature.rb', line 223

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



209
210
211
212
# File 'lib/feature.rb', line 209

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)


136
137
138
139
140
141
# File 'lib/feature.rb', line 136

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



143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/feature.rb', line 143

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



214
215
216
217
218
219
220
221
# File 'lib/feature.rb', line 214

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



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

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)


119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/feature.rb', line 119

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



91
92
93
# File 'lib/feature.rb', line 91

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

.gitlab_instanceObject



270
271
272
# File 'lib/feature.rb', line 270

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.



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

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



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

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

.log_feature_flag_states?(key) ⇒ Boolean

Returns:

  • (Boolean)


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

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

.logged_statesObject



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

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

.loggerObject



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

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

.opt_out(key, thing) ⇒ Object



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

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)


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

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)


107
108
109
110
111
112
# File 'lib/feature.rb', line 107

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



95
96
97
98
99
100
101
102
103
104
105
# File 'lib/feature.rb', line 95

def persisted_names
  return [] unless FlipperRecord.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



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

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

.register_definitionsObject



248
249
250
# File 'lib/feature.rb', line 248

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/index.html

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



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

def register_feature_groups; end

.register_hot_reloaderObject



252
253
254
255
256
# File 'lib/feature.rb', line 252

def register_hot_reloader
  return unless check_feature_flags_definition?

  Feature::Definition.register_hot_reloader!
end

.remove(key) ⇒ Object



228
229
230
231
232
233
234
# File 'lib/feature.rb', line 228

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

  log(key: key, action: __method__)

  with_feature(key, &:remove)
end

.remove_opt_out(key, thing) ⇒ Object



188
189
190
191
192
193
194
195
196
197
198
# File 'lib/feature.rb', line 188

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



236
237
238
239
# File 'lib/feature.rb', line 236

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