green_flag 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +7 -0
- data/Gemfile +17 -0
- data/Gemfile.lock +138 -0
- data/MIT-LICENSE +20 -0
- data/README.md +43 -0
- data/Rakefile +38 -0
- data/app/assets/images/green_flag/.gitkeep +0 -0
- data/app/assets/javascripts/green_flag/admin/features.js +353 -0
- data/app/assets/javascripts/green_flag/admin/rules.js +2 -0
- data/app/assets/javascripts/green_flag/application.js +14 -0
- data/app/assets/stylesheets/green_flag/admin/features.css.scss +36 -0
- data/app/assets/stylesheets/green_flag/admin/rules.css.scss +3 -0
- data/app/assets/stylesheets/green_flag/application.css +13 -0
- data/app/controllers/green_flag/admin/feature_decision_summaries_controller.rb +33 -0
- data/app/controllers/green_flag/admin/features_controller.rb +32 -0
- data/app/controllers/green_flag/admin/rule_lists_controller.rb +28 -0
- data/app/controllers/green_flag/admin/white_list_users_controller.rb +39 -0
- data/app/controllers/green_flag/site_visitor_management.rb +51 -0
- data/app/helpers/green_flag/application_helper.rb +4 -0
- data/app/models/green_flag/feature.rb +84 -0
- data/app/models/green_flag/feature_decision.rb +77 -0
- data/app/models/green_flag/feature_event.rb +9 -0
- data/app/models/green_flag/rule.rb +66 -0
- data/app/models/green_flag/site_visitor.rb +49 -0
- data/app/models/green_flag/user_group.rb +8 -0
- data/app/models/green_flag/visitor_group.rb +60 -0
- data/app/views/green_flag/admin/features/index.html.erb +14 -0
- data/app/views/green_flag/admin/features/show.html.erb +144 -0
- data/app/views/layouts/green_flag/application.html.erb +25 -0
- data/config/routes.rb +12 -0
- data/db/migrate/20140502112602_create_green_flag_site_visitors.rb +9 -0
- data/db/migrate/20140502221059_create_green_flag_features.rb +10 -0
- data/db/migrate/20140502221423_create_green_flag_feature_decisions.rb +13 -0
- data/db/migrate/20140505204611_add_visitor_code_to_site_visitors.rb +8 -0
- data/db/migrate/20140511045110_create_green_flag_rules.rb +12 -0
- data/db/migrate/20140513203728_set_default_percentage_in_green_flag_rules.rb +10 -0
- data/db/migrate/20140514202337_require_ordering_for_green_flag_rules.rb +13 -0
- data/db/migrate/20140516214909_add_restrictions_to_green_flag_rules.rb +13 -0
- data/db/migrate/20150211214159_create_green_flag_feature_events.rb +13 -0
- data/db/migrate/20150213191101_add_rule_id_to_green_flag_feature_decisions.rb +5 -0
- data/db/migrate/20150218035000_add_version_number_to_green_flag_rules.rb +9 -0
- data/db/migrate/20150218035805_add_version_number_to_green_flag_features.rb +9 -0
- data/db/migrate/20150218171852_add_version_number_to_green_flag_rules_indices.rb +19 -0
- data/green_flag.gemspec +34 -0
- data/green_flag.png +0 -0
- data/green_flag_small.png +0 -0
- data/lib/green_flag/engine.rb +16 -0
- data/lib/green_flag/version.rb +3 -0
- data/lib/green_flag.rb +4 -0
- data/lib/tasks/green_flag_tasks.rake +4 -0
- data/script/rails +8 -0
- data/spec/controllers/admin/feature_decision_summaries_controller_spec.rb +17 -0
- data/spec/controllers/admin/features_controller_spec.rb +21 -0
- data/spec/controllers/admin/rule_lists_controller_spec.rb +25 -0
- data/spec/controllers/admin/white_list_users_controller_spec.rb +15 -0
- data/spec/controllers/site_visitor_management_spec.rb +54 -0
- data/spec/dummy/README.rdoc +261 -0
- data/spec/dummy/Rakefile +7 -0
- data/spec/dummy/app/assets/javascripts/application.js +15 -0
- data/spec/dummy/app/assets/stylesheets/application.css +13 -0
- data/spec/dummy/app/controllers/application_controller.rb +7 -0
- data/spec/dummy/app/controllers/feature_check_controller.rb +10 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/mailers/.gitkeep +0 -0
- data/spec/dummy/app/models/.gitkeep +0 -0
- data/spec/dummy/app/models/user.rb +3 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/config/application.rb +65 -0
- data/spec/dummy/config/boot.rb +10 -0
- data/spec/dummy/config/database.yml +43 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +37 -0
- data/spec/dummy/config/environments/production.rb +67 -0
- data/spec/dummy/config/environments/test.rb +37 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/inflections.rb +15 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/secret_token.rb +7 -0
- data/spec/dummy/config/initializers/session_store.rb +8 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +5 -0
- data/spec/dummy/config/routes.rb +6 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/db/migrate/20150726195118_create_users.rb +8 -0
- data/spec/dummy/db/migrate/20150726204409_add_email_to_users.rb +5 -0
- data/spec/dummy/db/schema.rb +78 -0
- data/spec/dummy/lib/assets/.gitkeep +0 -0
- data/spec/dummy/log/.gitkeep +0 -0
- data/spec/dummy/log/development.log +8341 -0
- data/spec/dummy/log/test.log +34578 -0
- data/spec/dummy/public/404.html +26 -0
- data/spec/dummy/public/422.html +26 -0
- data/spec/dummy/public/500.html +25 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/dummy/script/rails +6 -0
- data/spec/dummy/tmp/cache/assets/C47/3D0/sprockets%2F8f17c33229239b023190617bf2e915a3 +0 -0
- data/spec/dummy/tmp/cache/assets/C81/770/sprockets%2Fdbcf34796b062155788f0b550808541a +0 -0
- data/spec/dummy/tmp/cache/assets/C89/D60/sprockets%2F73cd073739a0655341b7278fae57518f +0 -0
- data/spec/dummy/tmp/cache/assets/CB6/8F0/sprockets%2F5ea0f1f2583683678e122a9a9391a80f +0 -0
- data/spec/dummy/tmp/cache/assets/CD8/370/sprockets%2F357970feca3ac29060c1e3861e2c0953 +0 -0
- data/spec/dummy/tmp/cache/assets/CE7/FF0/sprockets%2Fe45f3a7675a8c5a5b064117792bf5e28 +0 -0
- data/spec/dummy/tmp/cache/assets/D07/670/sprockets%2F761d03a66b753d628feccd12072c814c +0 -0
- data/spec/dummy/tmp/cache/assets/D10/860/sprockets%2F4582878dbb5b72bfa76615d31b34ed51 +0 -0
- data/spec/dummy/tmp/cache/assets/D13/270/sprockets%2F701a30cd450ae3cfa114092bafc16004 +0 -0
- data/spec/dummy/tmp/cache/assets/D15/C10/sprockets%2F6f42f843c916d7864a0dfa912fb3194a +0 -0
- data/spec/dummy/tmp/cache/assets/D18/860/sprockets%2Fce00769800ae939cebb28501947ea96f +0 -0
- data/spec/dummy/tmp/cache/assets/D32/A10/sprockets%2F13fe41fee1fe35b49d145bcc06610705 +0 -0
- data/spec/dummy/tmp/cache/assets/D4A/970/sprockets%2F2a7d3b403cbdd8b59d57d26964ea5768 +0 -0
- data/spec/dummy/tmp/cache/assets/D4E/1B0/sprockets%2Ff7cbd26ba1d28d48de824f0e94586655 +0 -0
- data/spec/dummy/tmp/cache/assets/D59/090/sprockets%2F88788ba6a64e6279624e5c2ff7eead53 +0 -0
- data/spec/dummy/tmp/cache/assets/D5A/EA0/sprockets%2Fd771ace226fc8215a3572e0aa35bb0d6 +0 -0
- data/spec/dummy/tmp/cache/assets/D5C/330/sprockets%2Fd1b1c9a53f4a8a5827e5b02bf0e100e8 +0 -0
- data/spec/dummy/tmp/cache/assets/D68/760/sprockets%2Fea24808c41a3dff75b995b0f090e1a6a +0 -0
- data/spec/dummy/tmp/cache/assets/D6A/580/sprockets%2F18c12847aa1bb46ce9b5661f0e9e5fb0 +0 -0
- data/spec/dummy/tmp/cache/assets/DA1/210/sprockets%2F25a2979e392c8bc3adce7075cf19ab4b +0 -0
- data/spec/dummy/tmp/cache/assets/DA9/2C0/sprockets%2Ff95d82b2bbb6db8ffe1a87f67b415291 +0 -0
- data/spec/dummy/tmp/cache/assets/DB2/0C0/sprockets%2F95cf35cd3e97774df3c41ee0ef564a8d +0 -0
- data/spec/dummy/tmp/cache/assets/DDC/400/sprockets%2Fcffd775d018f68ce5dba1ee0d951a994 +0 -0
- data/spec/dummy/tmp/cache/assets/E04/890/sprockets%2F2f5173deea6c795b8fdde723bb4b63af +0 -0
- data/spec/dummy/tmp/cache/assets/E07/200/sprockets%2F82a8ce7f5bcfb07f773df4cbfeb04762 +0 -0
- data/spec/dummy/tmp/cache/assets/E34/D30/sprockets%2F99c2d0bbd78f1b867beeb3a2eefda618 +0 -0
- data/spec/factories/green_flag/feature.rb +7 -0
- data/spec/factories/green_flag/rule.rb +9 -0
- data/spec/features/admin_spec.rb +16 -0
- data/spec/features/visitor_spec.rb +36 -0
- data/spec/models/green_flag/feature_decision_spec.rb +102 -0
- data/spec/models/green_flag/feature_event_spec.rb +4 -0
- data/spec/models/green_flag/feature_spec.rb +135 -0
- data/spec/models/green_flag/rule_spec.rb +107 -0
- data/spec/models/green_flag/site_visitor_spec.rb +95 -0
- data/spec/models/green_flag/user_group_spec.rb +24 -0
- data/spec/models/green_flag/visitor_group_spec.rb +81 -0
- data/spec/spec_helper.rb +20 -0
- data/spec/support/controller_helpers.rb +7 -0
- metadata +359 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
|
3
|
+
* listed below.
|
|
4
|
+
*
|
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
|
6
|
+
* or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
|
|
7
|
+
*
|
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the top of the
|
|
9
|
+
* compiled file, but it's generally better to create a new file per style scope.
|
|
10
|
+
*
|
|
11
|
+
*= require_self
|
|
12
|
+
*= require_tree .
|
|
13
|
+
*/
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
class GreenFlag::Admin::FeatureDecisionSummariesController < ApplicationController
|
|
2
|
+
|
|
3
|
+
def show
|
|
4
|
+
feature_id = params[:feature_id]
|
|
5
|
+
feature = GreenFlag::Feature.find(feature_id)
|
|
6
|
+
|
|
7
|
+
render :json => summary(feature)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def update
|
|
11
|
+
feature_id = params[:feature_id]
|
|
12
|
+
feature = GreenFlag::Feature.find(feature_id)
|
|
13
|
+
|
|
14
|
+
if params[:forget_enabled]
|
|
15
|
+
feature.forget_non_manual_decisions!(true)
|
|
16
|
+
end
|
|
17
|
+
if params[:forget_disabled]
|
|
18
|
+
feature.forget_non_manual_decisions!(false)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
render :json => summary(feature)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def summary(feature)
|
|
27
|
+
{
|
|
28
|
+
enabled: feature.enabled_count,
|
|
29
|
+
disabled: feature.disabled_count,
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
class GreenFlag::Admin::FeaturesController < ApplicationController
|
|
2
|
+
|
|
3
|
+
layout 'green_flag/application'
|
|
4
|
+
|
|
5
|
+
def index
|
|
6
|
+
@features = GreenFlag::Feature.all
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def show
|
|
10
|
+
@feature = GreenFlag::Feature.find(params[:id])
|
|
11
|
+
@visitor_groups = GreenFlag::VisitorGroup.all.map { |group| { key: group.key, description: group.description } }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def current_visitor_status
|
|
15
|
+
@feature = GreenFlag::Feature.find(params[:id])
|
|
16
|
+
fd = GreenFlag::FeatureDecision.for_feature(@feature.id).where(site_visitor_id: current_site_visitor.id).first
|
|
17
|
+
render :json => { status: status_text(fd) }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def status_text(feature_decison)
|
|
23
|
+
if feature_decison.nil? || feature_decison.undecided?
|
|
24
|
+
"Undecided"
|
|
25
|
+
elsif feature_decison.enabled?
|
|
26
|
+
"Enabled"
|
|
27
|
+
else
|
|
28
|
+
"Disabled"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
class GreenFlag::Admin::RuleListsController < ApplicationController
|
|
2
|
+
|
|
3
|
+
def show
|
|
4
|
+
feature_id = params[:feature_id]
|
|
5
|
+
feature = GreenFlag::Feature.find(feature_id)
|
|
6
|
+
rules = feature.rules
|
|
7
|
+
|
|
8
|
+
render :json => rules.to_json(methods: :group_description)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def update
|
|
12
|
+
feature_id = params[:feature_id].to_i
|
|
13
|
+
rule_array = params['_json'] || []
|
|
14
|
+
|
|
15
|
+
filter_rules(rule_array)
|
|
16
|
+
|
|
17
|
+
rules = GreenFlag::Rule.set_rules!(feature_id, rule_array)
|
|
18
|
+
render :json => rules.to_json(methods: :group_description)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def filter_rules(rule_array)
|
|
24
|
+
rule_array.each do |rule|
|
|
25
|
+
rule.delete('group_description')
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
class GreenFlag::Admin::WhiteListUsersController < ApplicationController
|
|
2
|
+
|
|
3
|
+
def index
|
|
4
|
+
feature_id = params[:feature_id]
|
|
5
|
+
users = GreenFlag::FeatureDecision.whitelisted_users(feature_id)
|
|
6
|
+
|
|
7
|
+
users.each { |u| u.include_root_in_json = false }
|
|
8
|
+
|
|
9
|
+
respond_to do |format|
|
|
10
|
+
format.js { render :json => users.to_json }
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def create
|
|
15
|
+
feature_id = params[:feature_id]
|
|
16
|
+
feature = GreenFlag::Feature.find(feature_id)
|
|
17
|
+
|
|
18
|
+
user = User.where(email: params[:email]).first
|
|
19
|
+
GreenFlag::FeatureDecision.whitelist_user!(feature.code, user)
|
|
20
|
+
|
|
21
|
+
user.include_root_in_json = false
|
|
22
|
+
|
|
23
|
+
respond_to do |format|
|
|
24
|
+
format.js { render :json => user.to_json }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def destroy
|
|
29
|
+
feature_id = params[:feature_id]
|
|
30
|
+
user_id = params[:id]
|
|
31
|
+
|
|
32
|
+
fd = GreenFlag::FeatureDecision.for_user(user_id).for_feature(feature_id).first
|
|
33
|
+
fd.destroy
|
|
34
|
+
|
|
35
|
+
respond_to do |format|
|
|
36
|
+
format.js { render :json => '' }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module GreenFlag::SiteVisitorManagement
|
|
2
|
+
COOKIE_NAME = 'green_flag_site_visitor'
|
|
3
|
+
|
|
4
|
+
def self.included(base)
|
|
5
|
+
base.before_filter :set_site_visitor
|
|
6
|
+
base.helper_method :feature_enabled?
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# Ensure we have a cookie
|
|
10
|
+
def set_site_visitor
|
|
11
|
+
ensure_code_cookie
|
|
12
|
+
record_login(current_user) if current_user
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Make sure the current SiteVisitor is the correct visitor for this user
|
|
16
|
+
def record_login(user)
|
|
17
|
+
self.current_site_visitor = GreenFlag::SiteVisitor.for_user!(user, current_site_visitor)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Finds or creates a GreenFlag::SiteVisitor
|
|
21
|
+
def current_site_visitor
|
|
22
|
+
@current_site_visitor ||= begin
|
|
23
|
+
code = ensure_code_cookie
|
|
24
|
+
GreenFlag::SiteVisitor.for_visitor_code!(code)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def feature_enabled?(feature_code)
|
|
29
|
+
@features_enabled ||= {}
|
|
30
|
+
if @features_enabled[feature_code].nil?
|
|
31
|
+
visitor_id = current_site_visitor.id
|
|
32
|
+
@features_enabled[feature_code] = GreenFlag::FeatureDecision.feature_enabled?(feature_code, visitor_id)
|
|
33
|
+
end
|
|
34
|
+
@features_enabled[feature_code]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def current_site_visitor=(visitor)
|
|
40
|
+
@current_site_visitor = visitor
|
|
41
|
+
cookies.permanent[COOKIE_NAME] = current_site_visitor.visitor_code
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def ensure_code_cookie
|
|
45
|
+
unless cookies[COOKIE_NAME]
|
|
46
|
+
cookies.permanent[COOKIE_NAME] = GreenFlag::SiteVisitor.new_code
|
|
47
|
+
end
|
|
48
|
+
cookies[COOKIE_NAME]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
class GreenFlag::Feature < ActiveRecord::Base
|
|
2
|
+
attr_protected #none
|
|
3
|
+
|
|
4
|
+
has_many :rules, :conditions => proc { "version_number = #{self.version_number}" }, order: 'order_by'
|
|
5
|
+
has_many :feature_decisions
|
|
6
|
+
has_many :feature_events
|
|
7
|
+
|
|
8
|
+
validates_presence_of :code
|
|
9
|
+
|
|
10
|
+
self.include_root_in_json = false
|
|
11
|
+
|
|
12
|
+
def self.for_code!(code)
|
|
13
|
+
feature = where(code: code.to_s).first
|
|
14
|
+
unless feature
|
|
15
|
+
feature = create!(code: code.to_s)
|
|
16
|
+
end
|
|
17
|
+
feature
|
|
18
|
+
rescue ActiveRecord::RecordNotUnique, PG::Error => ex
|
|
19
|
+
# Race condition
|
|
20
|
+
where(code: code.to_s).first || raise
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def decide_if_enabled_for_visitor(site_visitor_id)
|
|
24
|
+
fd_query = GreenFlag::FeatureDecision.where(feature_id: id, site_visitor_id: site_visitor_id)
|
|
25
|
+
feature_decision = fd_query.first_or_initialize
|
|
26
|
+
|
|
27
|
+
if feature_decision.undecided?
|
|
28
|
+
decide_feature_decision(feature_decision)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
feature_decision
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def enabled_count
|
|
35
|
+
feature_decisions.enabled.count
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def disabled_count
|
|
39
|
+
feature_decisions.disabled.count
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def forget_non_manual_decisions!(enabled)
|
|
43
|
+
non_manual_fds = feature_decisions.non_manual.where(enabled: enabled)
|
|
44
|
+
|
|
45
|
+
if non_manual_fds.count > 0
|
|
46
|
+
create_feature_event(enabled, non_manual_fds.count)
|
|
47
|
+
non_manual_fds.delete_all
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def latest_version
|
|
52
|
+
last_rule = GreenFlag::Rule.order('version_number DESC').where(feature_id: id).first
|
|
53
|
+
|
|
54
|
+
last_rule.present? ? last_rule.version_number : version_number
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def decide_feature_decision(feature_decision)
|
|
60
|
+
matched_rule = rules.find { |rule| rule.applies_to?(feature_decision.site_visitor) }
|
|
61
|
+
|
|
62
|
+
if matched_rule
|
|
63
|
+
feature_decision.rule_id = matched_rule.id
|
|
64
|
+
feature_decision.enabled = !!matched_rule.decision?
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
saved_feature_decision(feature_decision)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def saved_feature_decision(feature_decision)
|
|
71
|
+
save_result = feature_decision.safe_save!
|
|
72
|
+
|
|
73
|
+
if save_result == true
|
|
74
|
+
feature_decision
|
|
75
|
+
else
|
|
76
|
+
save_result
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def create_feature_event(enabled, count)
|
|
81
|
+
event_type_code = (enabled == true ? GreenFlag::FeatureEvent::ENABLED_DECISIONS_FORGOTTEN : GreenFlag::FeatureEvent::DISABLED_DECISIONS_FORGOTTEN)
|
|
82
|
+
GreenFlag::FeatureEvent.create!(feature_id: id, event_type_code: event_type_code, count: count)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
class GreenFlag::FeatureDecision < ActiveRecord::Base
|
|
2
|
+
|
|
3
|
+
attr_protected #none
|
|
4
|
+
|
|
5
|
+
validates_presence_of :feature_id, :site_visitor_id
|
|
6
|
+
|
|
7
|
+
belongs_to :feature
|
|
8
|
+
belongs_to :site_visitor
|
|
9
|
+
belongs_to :rule
|
|
10
|
+
|
|
11
|
+
has_one :user, through: :site_visitor
|
|
12
|
+
|
|
13
|
+
scope :enabled, -> { where(enabled: true) }
|
|
14
|
+
scope :disabled, -> { where(enabled: false) }
|
|
15
|
+
scope :whitelisted, ->(feature_id) {
|
|
16
|
+
for_feature(feature_id).enabled.where(manual: true).order(:created_at)
|
|
17
|
+
}
|
|
18
|
+
scope :for_user, ->(user_id) {
|
|
19
|
+
joins(:site_visitor).where('green_flag_site_visitors.user_id = ?', user_id)
|
|
20
|
+
}
|
|
21
|
+
scope :for_feature, ->(feature_id) {
|
|
22
|
+
where(feature_id: feature_id)
|
|
23
|
+
}
|
|
24
|
+
scope :non_manual, -> { where('manual is not true') }
|
|
25
|
+
|
|
26
|
+
# Check if the feature is enabled, AND store the result as a FeatureDecision
|
|
27
|
+
def self.feature_enabled?(feature_code, site_visitor_id)
|
|
28
|
+
feature = GreenFlag::Feature.for_code!(feature_code)
|
|
29
|
+
fd = feature.decide_if_enabled_for_visitor(site_visitor_id)
|
|
30
|
+
|
|
31
|
+
fd.enabled
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.feature_enabled_for_user?(feature_code, user)
|
|
35
|
+
feature_enabled? feature_code, GreenFlag::SiteVisitor.for_user!(user).id
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Force the given user to have the feature enabled
|
|
39
|
+
def self.whitelist_user!(feature_code, user)
|
|
40
|
+
ensure_feature_enabled(feature_code, user, true)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.ensure_feature_enabled(feature_code, user, manual=false)
|
|
44
|
+
feature = GreenFlag::Feature.for_code!(feature_code)
|
|
45
|
+
visitor = GreenFlag::SiteVisitor.for_user!(user)
|
|
46
|
+
|
|
47
|
+
fd = GreenFlag::FeatureDecision.where(feature_id: feature.id,
|
|
48
|
+
site_visitor_id: visitor.id).first_or_initialize
|
|
49
|
+
|
|
50
|
+
unless fd.enabled? && fd.manual == manual
|
|
51
|
+
fd.enabled = true
|
|
52
|
+
fd.manual = manual
|
|
53
|
+
fd.save!
|
|
54
|
+
end
|
|
55
|
+
fd
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.whitelisted_users(feature_id)
|
|
59
|
+
# inefficient, but shouldn't matter.
|
|
60
|
+
feature_decisions = whitelisted(feature_id).joins(:site_visitor => :user).all
|
|
61
|
+
users = feature_decisions.map(&:user)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def undecided?
|
|
65
|
+
self.enabled.nil?
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def safe_save!
|
|
69
|
+
begin
|
|
70
|
+
save!
|
|
71
|
+
true
|
|
72
|
+
rescue ActiveRecord::RecordNotUnique, PG::Error => ex
|
|
73
|
+
# Race condition
|
|
74
|
+
self.class.where(feature_id: feature_id, site_visitor_id: site_visitor_id).first || raise
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
class GreenFlag::FeatureEvent < ActiveRecord::Base
|
|
2
|
+
attr_accessible :event_type_code, :feature_id, :count
|
|
3
|
+
|
|
4
|
+
validates :event_type_code, presence: true
|
|
5
|
+
validates :feature_id, presence: true
|
|
6
|
+
|
|
7
|
+
ENABLED_DECISIONS_FORGOTTEN = 0
|
|
8
|
+
DISABLED_DECISIONS_FORGOTTEN = 1
|
|
9
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
class GreenFlag::Rule < ActiveRecord::Base
|
|
2
|
+
attr_protected # none
|
|
3
|
+
|
|
4
|
+
validates :feature_id, presence: true
|
|
5
|
+
validates :group_key, presence: true
|
|
6
|
+
validates :order_by, presence: true
|
|
7
|
+
validates :percentage, presence: true,
|
|
8
|
+
numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 100 }
|
|
9
|
+
|
|
10
|
+
self.include_root_in_json = false
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
public
|
|
14
|
+
|
|
15
|
+
def set_rules!(feature_id, rules_array)
|
|
16
|
+
return [] if rules_array.empty?
|
|
17
|
+
|
|
18
|
+
rules = create_new_rules(feature_id, rules_array)
|
|
19
|
+
|
|
20
|
+
transaction { rules.each(&:save!) }
|
|
21
|
+
|
|
22
|
+
rules
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def create_new_rules(feature_id, rules_array)
|
|
28
|
+
new_version_number = increment_feature_version(feature_id)
|
|
29
|
+
|
|
30
|
+
rules_array.map do |rule_attributes|
|
|
31
|
+
rule_attributes[:feature_id] = feature_id
|
|
32
|
+
create_new_rule_version(new_version_number, rule_attributes)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def increment_feature_version(feature_id)
|
|
37
|
+
feature = GreenFlag::Feature.find(feature_id)
|
|
38
|
+
|
|
39
|
+
feature.update_attributes(version_number: feature.latest_version + 1)
|
|
40
|
+
feature.version_number
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def create_new_rule_version(new_version_number, rule_attributes)
|
|
44
|
+
GreenFlag::Rule.new(rule_attributes.except(:id).merge(version_number: new_version_number))
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def applies_to?(visitor)
|
|
49
|
+
visitor_group.includes_visitor?(visitor, self)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def decision?
|
|
53
|
+
r = Random.rand(100)
|
|
54
|
+
r < percentage
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def group_description
|
|
58
|
+
visitor_group.description
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def visitor_group
|
|
64
|
+
GreenFlag::VisitorGroup.for_key(group_key)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# A SiteVisitor is a unique visitor to the site.
|
|
2
|
+
class GreenFlag::SiteVisitor < ActiveRecord::Base
|
|
3
|
+
|
|
4
|
+
attr_protected # none
|
|
5
|
+
|
|
6
|
+
belongs_to :user
|
|
7
|
+
has_many :feature_decisions
|
|
8
|
+
|
|
9
|
+
# Finds, updates, or creates a SiteVisitor for the given user.
|
|
10
|
+
# If the user has an existing SiteVisitor, return that
|
|
11
|
+
# Otherwise, check if the given SiteVisitor can be assigned to the user.
|
|
12
|
+
# Otherwise, create a brand new SiteVisitor
|
|
13
|
+
def self.for_user!(user, visitor_to_check=nil)
|
|
14
|
+
where(user_id: user.id).first ||
|
|
15
|
+
assign_visitor_if_available(visitor_to_check, user) ||
|
|
16
|
+
create!(user_id: user.id, visitor_code: new_code)
|
|
17
|
+
rescue ActiveRecord::RecordNotUnique, PG::Error => ex
|
|
18
|
+
# Race condition - some other request created/updated a SiteVisitor with our user's id
|
|
19
|
+
where(user_id: user.id).first || raise
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.for_visitor_code!(code)
|
|
23
|
+
where(visitor_code: code).first_or_create!
|
|
24
|
+
rescue ActiveRecord::RecordNotUnique, PG::Error => ex
|
|
25
|
+
# Race condition
|
|
26
|
+
where(visitor_code: code).first || raise
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.new_code
|
|
30
|
+
SecureRandom.uuid
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# If GreenFlag is added to an existing app,
|
|
34
|
+
# then some users might be older than their corresponding SiteVisitors
|
|
35
|
+
def first_visited_at
|
|
36
|
+
[created_at, user.try(:created_at), DateTime.now].compact.min
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def self.assign_visitor_if_available(visitor, user)
|
|
42
|
+
if visitor && visitor.user_id.nil?
|
|
43
|
+
visitor.update_attribute(:user_id, user.id)
|
|
44
|
+
return visitor
|
|
45
|
+
end
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
class GreenFlag::VisitorGroup
|
|
2
|
+
|
|
3
|
+
class MultipleGroupsError < StandardError; end
|
|
4
|
+
MUTEX = Mutex.new
|
|
5
|
+
|
|
6
|
+
attr_reader :key, :description
|
|
7
|
+
|
|
8
|
+
def self.define(&block)
|
|
9
|
+
instance_eval(&block)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.group(key, description=nil, type=GreenFlag::VisitorGroup, &block)
|
|
13
|
+
new_group = nil
|
|
14
|
+
MUTEX.synchronize do
|
|
15
|
+
key_exists = for_key(key)
|
|
16
|
+
if key_exists
|
|
17
|
+
raise MultipleGroupsError.new "Two groups with key :#{key} were defined. Rename one of them!"
|
|
18
|
+
end
|
|
19
|
+
new_group = type.new(key, description, &block)
|
|
20
|
+
@groups ||= []
|
|
21
|
+
@groups << new_group
|
|
22
|
+
end
|
|
23
|
+
new_group
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.user_group(key, description=nil, &block)
|
|
27
|
+
group(key, description, GreenFlag::UserGroup, &block)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.for_key(key)
|
|
31
|
+
all.find { |g| g.key.to_s == key.to_s }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.clear!
|
|
35
|
+
@groups = []
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.all
|
|
39
|
+
@groups || []
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.keys
|
|
43
|
+
all.map(&:key)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def initialize(key, description=nil, &block)
|
|
47
|
+
self.key = key.to_s
|
|
48
|
+
self.description = description
|
|
49
|
+
self.evaluator = block
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def includes_visitor?(visitor, rule=nil)
|
|
53
|
+
evaluator.call(visitor, rule)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
attr_writer :key, :description, :evaluator
|
|
58
|
+
attr_reader :evaluator
|
|
59
|
+
|
|
60
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
|
|
2
|
+
<div class="header clearfix">
|
|
3
|
+
<h1><%= link_to 'Green Flag', admin_features_path %></h1>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<div class="row">
|
|
7
|
+
<% @features.each do |feature| %>
|
|
8
|
+
<div class="col-md-6">
|
|
9
|
+
<h4><%= link_to feature.code, admin_feature_path(feature) %></h4>
|
|
10
|
+
<p><%= feature.description %></p>
|
|
11
|
+
</div>
|
|
12
|
+
<% end %>
|
|
13
|
+
|
|
14
|
+
</div>
|