Module: Feature

Defined in:
lib/feature/shared.rb,
lib/feature.rb,
lib/feature/gitaly.rb,
lib/feature/logger.rb,
lib/feature/definition.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, Definition, FlipperFeature, FlipperGate, FlipperRequest, Gitaly, Logger, OptOut, Target

Constant Summary collapse

InvalidFeatureFlagError =

rubocop:disable Lint/InheritException

Class.new(Exception)
InvalidOperation =

rubocop:disable Lint/InheritException

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

Class Method Summary collapse

Class Method Details

.allObject



57
58
59
# File 'lib/feature.rb', line 57

def all
  flipper.features.to_a
end

.current_requestObject



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

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



131
132
133
134
135
# File 'lib/feature.rb', line 131

def disable(key, thing = false)
  log(key: key, action: __method__, thing: thing)

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

.disable_percentage_of_actors(key) ⇒ Object



188
189
190
191
# File 'lib/feature.rb', line 188

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



174
175
176
177
# File 'lib/feature.rb', line 174

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

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

Returns:

  • (Boolean)


113
114
115
116
# File 'lib/feature.rb', line 113

def disabled?(key, thing = nil, type: :development, default_enabled_if_undefined: nil)
  # 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



118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/feature.rb', line 118

def enable(key, thing = true)
  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



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

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



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

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: :development, 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)


91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/feature.rb', line 91

def enabled?(key, thing = nil, type: :development, default_enabled_if_undefined: nil)
  if check_feature_flags_definition?
    if thing && !thing.respond_to?(:flipper_id) && !thing.is_a?(Flipper::Types::Group)
      raise InvalidFeatureFlagError,
        "The thing '#{thing.class.name}' for feature flag '#{key}' needs to include `FeatureGate` or implement `flipper_id`"
    end

    Feature::Definition.valid_usage!(key, type: type)
  end

  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



63
64
65
# File 'lib/feature.rb', line 63

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

.log_feature_flag_state(key, feature_value) ⇒ Object



239
240
241
# File 'lib/feature.rb', line 239

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

.log_feature_flag_states?(key) ⇒ Boolean

Returns:

  • (Boolean)


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

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

.logged_statesObject



243
244
245
# File 'lib/feature.rb', line 243

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

.loggerObject



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

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

.opt_out(key, thing) ⇒ Object



146
147
148
149
150
151
152
153
# File 'lib/feature.rb', line 146

def opt_out(key, 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)


137
138
139
140
141
142
143
144
# File 'lib/feature.rb', line 137

def opted_out?(key, 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)


79
80
81
82
83
84
# File 'lib/feature.rb', line 79

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



67
68
69
70
71
72
73
74
75
76
77
# File 'lib/feature.rb', line 67

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

.register_definitionsObject



213
214
215
# File 'lib/feature.rb', line 213

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.



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

def register_feature_groups; end

.register_hot_reloaderObject



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

def register_hot_reloader
  return unless check_feature_flags_definition?

  Feature::Definition.register_hot_reloader!
end

.remove(key) ⇒ Object



193
194
195
196
197
198
199
# File 'lib/feature.rb', line 193

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

  log(key: key, action: __method__)

  with_feature(key, &:remove)
end

.remove_opt_out(key, thing) ⇒ Object



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

def remove_opt_out(key, 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



201
202
203
204
# File 'lib/feature.rb', line 201

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