green_flag 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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>
|