Files
Sony MathewandGitHub 873d16f54c feat: Extend account feature flag storage (#14947)
# Pull Request Template

## Description

Extends account-level feature flags by adding a second bigint bitset
column, `feature_flags_ext_2`, while preserving the existing
`flag_shih_tzu` feature check and enable/disable APIs. Existing flags
continue to live on `feature_flags`; future flags can opt into the
extension column through `config/features.yml` metadata.

Fixes
[CW-7238](https://linear.app/chatwoot/issue/CW-7238/feature-flag-extension)

## Type of change

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [ ] This change requires a documentation update

## How Has This Been Tested?

- `eval "$(rbenv init -)" && RAILS_ENV=test
POSTGRES_DATABASE=chatwoot_test_31a6 bundle exec rspec
spec/models/concerns/featurable_spec.rb spec/models/account_spec.rb
spec/lib/config_loader_spec.rb
spec/controllers/platform/api/v1/accounts_controller_spec.rb
spec/controllers/super_admin/accounts_controller_spec.rb
spec/enterprise/models/account_spec.rb` - 144 examples, 0 failures
- `eval "$(rbenv init -)" && bundle exec rubocop
app/models/concerns/featurable.rb app/models/account.rb
db/migrate/20260706215758_add_feature_flags_ext_2_to_accounts.rb
spec/models/concerns/featurable_spec.rb spec/models/account_spec.rb
spec/lib/config_loader_spec.rb spec/enterprise/models/account_spec.rb` -
7 files inspected, no offenses detected
- `ruby -ryaml -e "features =
YAML.safe_load(File.read('config/features.yml')); abort unless
features.size == 63; puts features.group_by { |f| f['column'] ||
'feature_flags' }.transform_values(&:size).inspect"` - `{"feature_flags"
=> 63}`
- `git diff --check`

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules
2026-07-08 17:38:26 +05:30

123 lines
3.2 KiB
Ruby

module Featurable
extend ActiveSupport::Concern
DEFAULT_FEATURE_FLAG_COLUMN = 'feature_flags'.freeze
FEATURE_FLAG_COLUMNS = [DEFAULT_FEATURE_FLAG_COLUMN, 'feature_flags_ext_1'].freeze
MAX_FEATURES_PER_COLUMN = 63
QUERY_MODE = {
flag_query_mode: :bit_operator,
check_for_column: false
}.freeze
FEATURE_LIST = YAML.safe_load(Rails.root.join('config/features.yml').read).freeze
def self.feature_flag_mappings_for(feature_list)
features_by_column = feature_list.group_by { |feature| feature['column'].presence || DEFAULT_FEATURE_FLAG_COLUMN }
mappings = FEATURE_FLAG_COLUMNS.index_with do |column|
features = features_by_column.delete(column) || []
validate_feature_count!(column, features)
features.each_with_index.to_h do |feature, index|
[index + 1, "feature_#{feature['name']}".to_sym]
end
end
validate_feature_columns!(features_by_column)
mappings
end
def self.validate_feature_count!(column, features)
return if features.size <= MAX_FEATURES_PER_COLUMN
raise ArgumentError, "Account feature flag column #{column} supports up to #{MAX_FEATURES_PER_COLUMN} features"
end
def self.validate_feature_columns!(features_by_column)
return if features_by_column.blank?
invalid_columns = features_by_column.keys.join(', ')
raise ArgumentError, "Unknown account feature flag column: #{invalid_columns}"
end
FEATURES_BY_COLUMN = feature_flag_mappings_for(FEATURE_LIST).freeze
included do
include FlagShihTzu
FEATURE_FLAG_COLUMNS.each do |column|
has_flags FEATURES_BY_COLUMN.fetch(column).merge(column: column).merge(QUERY_MODE)
end
before_create :enable_default_features
define_method :all_feature_flags do
FEATURE_FLAG_COLUMNS.flat_map { |column| all_flags(column) }
end
define_method :selected_feature_flags do
FEATURE_FLAG_COLUMNS.flat_map { |column| selected_flags(column) }
end
define_method :selected_feature_flags= do |chosen_flags|
FEATURE_FLAG_COLUMNS.each { |column| unselect_all_flags(column) }
return if chosen_flags.nil?
chosen_flags.each do |selected_flag|
enable_flag(selected_flag.to_sym) if selected_flag.present?
end
end
end
def enable_features(*names)
names.each do |name|
send("feature_#{name}=", true)
end
end
def enable_features!(*names)
enable_features(*names)
save
end
def disable_features(*names)
names.each do |name|
send("feature_#{name}=", false)
end
end
def disable_features!(*names)
disable_features(*names)
save
end
def feature_enabled?(name)
send("feature_#{name}?")
end
def all_features
FEATURE_LIST.pluck('name').index_with do |feature_name|
feature_enabled?(feature_name)
end
end
def enabled_features
all_features.select { |_feature, enabled| enabled == true }
end
def disabled_features
all_features.select { |_feature, enabled| enabled == false }
end
private
def enable_default_features
config = InstallationConfig.find_by(name: 'ACCOUNT_LEVEL_FEATURE_DEFAULTS')
return true if config.blank?
features_to_enabled = config.value.select { |f| f[:enabled] }.pluck(:name)
enable_features(*features_to_enabled)
end
end