Repository: airbnb/trebuchet
Branch: master
Commit: d5cbd014fa6a
Files: 81
Total size: 112.8 KB
Directory structure:
gitextract_xbvggvpr/
├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── Gemfile
├── README.md
├── Rakefile
├── app/
│ ├── controllers/
│ │ └── trebuchet_rails/
│ │ └── features_controller.rb
│ ├── helpers/
│ │ └── trebuchet_helper.rb
│ └── views/
│ ├── layouts/
│ │ └── trebuchet.html.erb
│ └── trebuchet_rails/
│ ├── features/
│ │ ├── index.html.erb
│ │ └── timeline.html.erb
│ └── trebuchet.css
├── config/
│ └── routes.rb
├── init.rb
├── lib/
│ ├── trebuchet/
│ │ ├── action_controller.rb
│ │ ├── action_controller_filter.rb
│ │ ├── backend/
│ │ │ ├── disabled.rb
│ │ │ ├── memcached.rb
│ │ │ ├── memory.rb
│ │ │ ├── redis.rb
│ │ │ ├── redis_cached.rb
│ │ │ └── redis_hammerspaced.rb
│ │ ├── backend.rb
│ │ ├── error.rb
│ │ ├── feature/
│ │ │ └── stubbing.rb
│ │ ├── feature.rb
│ │ ├── state.rb
│ │ ├── strategy/
│ │ │ ├── base.rb
│ │ │ ├── custom.rb
│ │ │ ├── custom_request_aware.rb
│ │ │ ├── default.rb
│ │ │ ├── everyone.rb
│ │ │ ├── experiment.rb
│ │ │ ├── hostname.rb
│ │ │ ├── invalid.rb
│ │ │ ├── logic_and.rb
│ │ │ ├── logic_base.rb
│ │ │ ├── logic_not.rb
│ │ │ ├── logic_or.rb
│ │ │ ├── multiple.rb
│ │ │ ├── nobody.rb
│ │ │ ├── per_denomination.rb
│ │ │ ├── percent.rb
│ │ │ ├── percent_deprecated.rb
│ │ │ ├── stub.rb
│ │ │ ├── user_id.rb
│ │ │ ├── visitor_experiment.rb
│ │ │ ├── visitor_percent.rb
│ │ │ └── visitor_percent_deprecated.rb
│ │ ├── strategy.rb
│ │ └── version.rb
│ ├── trebuchet.rb
│ ├── trebuchet_rails/
│ │ └── engine.rb
│ └── trebuchet_rails.rb
├── spec/
│ ├── custom_request_aware_strategy_spec.rb
│ ├── custom_strategy_spec.rb
│ ├── default_strategy_spec.rb
│ ├── disabled_backend_spec.rb
│ ├── everyone_strategy_spec.rb
│ ├── experiment_strategy_spec.rb
│ ├── feature_spec.rb
│ ├── logic_and_strategy_spec.rb
│ ├── logic_base_strategy_spec.rb
│ ├── logic_not_strategy_spec.rb
│ ├── logic_or_strategy_spec.rb
│ ├── multiple_strategy_spec.rb
│ ├── nobody_strategy_spec.rb
│ ├── per_denomination_strategy_spec.rb
│ ├── percent_deprecated_strategy_spec.rb
│ ├── percent_strategy_spec.rb
│ ├── redis_backend_spec.rb
│ ├── redis_hammerspaced_spec.rb
│ ├── spec_helper.rb
│ ├── stubbing_spec.rb
│ ├── trebuchet_spec.rb
│ ├── user.rb
│ ├── user_id_strategy_spec.rb
│ ├── visitor_experiment_strategy_spec.rb
│ ├── visitor_percent_deprecated_strategy_spec.rb
│ └── visitor_percent_strategy_spec.rb
└── trebuchet.gemspec
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
*.gem
.bundle
Gemfile.lock
pkg/*
.idea
.rspec
================================================
FILE: .travis.yml
================================================
language: ruby
cache: bundler
sudo: false
rvm:
- 1.9.3
- 2.1
- 2.2
- 2.3
- 2.4
- 2.5
- 2.6
before_install:
- gem install bundler -v 1.17.3
================================================
FILE: CHANGELOG.md
================================================
## 0.12.0 (Nov 26, 2019)
- Contain fiber-unsafe state of the main Trebuchet class into a state object and optionally store it as a thread/fiber local.
## 0.11.0 (Aug 15, 2019)
- add Rails 5 compatibility
## 0.10.1 (Jul 13, 2018)
- add PerDenomination strategy
## 0.10.0 (Nov 17, 2017)
- Cache Trebuchet#launch?
## 0.9.18 (Sep 18, 2017)
- Do not expose internal format
## 0.9.17 (Aug 15, 2017)
- adds CustomRequestAware strategy
## 0.9.16 (Aug 9, 2017)
- Don't raise an error if expiration date has not been previously set
## 0.9.15 (Aug 9, 2017)
- Return payload for expiration date accessor
## 0.9.14 (Aug 9, 2017)
- Expose expiration date accessor
## 0.9.13 (Aug 8, 2017)
- Fix a typo
## 0.9.12 (Aug 3, 2017)
- Add setter for expiration date
## 0.9.11 (Nov 16, 2016)
- minor change: reduce number of times backend is hit in the launch methods
## 0.9.10 (Nov 14, 2016)
- fix logical operators for logged out visitors
## 0.9.9 (Sep 15, 2016)
- minor change: keep raw option hash in logic strategies for editing purpose
## 0.9.8 (July 27, 2016)
- convert deserialized strategy names to symbols
## 0.9.7 (July 20, 2016)
- logic operation strategies, e.g. AND, OR, and NOT
## 0.9.6 (April 5, 2016)
- add :add_comment method to the Feature class
## 0.9.2 (October 11, 2015)
- add :nobody and :everybody strategies
- cache features
- make :default strategy a singleton
## 0.9.1 (Jul 22, 2015)
- fix last_update type in redis_hammerspaced
## 0.9.0 (Jul 22, 2015)
- adds update_hammerspace method so we can sync redis into local hammerspace
## 0.8.1 (Jul 21, 2015)
- update redis backend to update a sentinel to indicate modifications
## 0.8.0 (Jul 20, 2015)
- adds a new backened redis_hammerspaced
## 0.6.3 (Sep 7, 2013)
- reimplements the percent strategy
- adds per-feature stubbing interface
## 0.0.4 (Dec 6, 2011)
- add experiment strategy
- fix percentage offset
- fix Redis/Memcache dependency issue
================================================
FILE: Gemfile
================================================
source "http://rubygems.org"
# Specify your gem's dependencies in trebuchet.gemspec
gemspec
current_ruby = Gem::Version.new(RUBY_VERSION)
if current_ruby < Gem::Version.new("2.2")
gem "rake", "< 12.3"
end
================================================
FILE: README.md
================================================
Trebuchet
=========
Trebuchet launches features at people. Wisely choose a strategy, aim, and launch!
Installation
------------
Trebuchet can be used with Rails or standalone.
To use with Rails:
gem 'trebuchet', :require => 'trebuchet_rails'
Setup
-----
Trebuchet defaults to storing data in memory, or can be used with Redis or Memcache as a data store:
Trebuchet.set_backend :memcached
Trebuchet.set_backend :redis, :client => Redis.new(:host => 'example.com')
Trebuchet.set_backend :redis_cached, :client => Redis.new(:host => 'example.com')
A Rails initializer is a great spot for this. You may want to use a few other settings, either hardcoded values or procs (eval'd in the context of the controller):
Trebuchet.admin_view = proc { current_user.try(:admin?) } # /trebuchet admin interface access
Trebuchet.time_zone = proc { current_user.time_zone } # or just "Mountain Time (US & Canada)"
Aim
---
Trebuchet can be aimed while your application is running. The syntax is:
Trebuchet.aim('awesome_feature', :percent, 1)
Which will launch 'awesome_feature' to 1% of users.
Another builtin strategy allows launching to particular user IDs:
Trebuchet.aim('awesome_feature', :users, [23, 42])
You can also combine multiple strategies, in which case the feature is launched if any of them is true:
Trebuchet.feature('awesome_feature').aim(:percent, 1).aim(:users, [23, 42])
If you don't aim Trebuchet for a feature, the default action is not to launch it to anyone.
Launch
------
In a view, do this:
<% trebuchet.launch('time_machine') do %>
<p>Welcome to the future!</p>
<% end %>
The code between do .. end will only run if the strategy for 'time_machine' allows launching to current_user.
You can also use it in a controller:
def index
trebuchet.launch('time_machine') do
@time_machine = TimeMachine.new
end
end
Custom Strategies
-----------------
Trebuchet ships with a number of default strategies but you can also define your own custom strategies like so:
Trebuchet.define_strategy(:admins) do |user|
!!(user && user.has_role?(:admin))
end
controller.current_user is yielded to the block and it should return true for users you want to launch to.
You can use parameters with custom strategies too:
Trebuchet.define_strategy(:markets) do |user, markets|
markets.include?(user.market)
end
Like parameters for builtin strategies, these can be changed while the application is running. For example:
Trebuchet.aim('time_machine', :markets, ['San Francisco', 'New York City'])
When using Trebuchet together with Rails, a good place to define custom strategies is in an initializer.
Visitor Strategy
----------------
Trebuchet can be used to launch to visitors (no user object present).
First, set the visitor id either directly (in a before filter) or as a proc:
Trebuchet.visitor_id = 123
Trebuchet.visitor_id = proc { |request| request && request.cookies[:visitor] && request.cookies[:visitor].hash }
If you're using a proc, Trebuchet passes in the request object. It expects that the proc returns an integer.
If it returns anything else, Trebuchet will not launch.
Fiber and Thread Safety
-------------
Trebuchet stores global state such as `Trebuchet.current` which is thread and fiber unsafe behavior. In order to use these
features in a fiber or threaded environment, `Trebuchet.threadsafe_state = true` will cause Trebuchet to store these values
in a thread-local state object instead. This is not the default for backward compatability reasons.
================================================
FILE: Rakefile
================================================
require 'bundler'
Bundler::GemHelper.install_tasks
task :spec do
system 'rspec ./spec'
end
task :default => :spec
================================================
FILE: app/controllers/trebuchet_rails/features_controller.rb
================================================
module TrebuchetRails
class FeaturesController < ApplicationController
if Rails::VERSION::MAJOR >= 5
before_action :control_access, :get_time_zone
else
before_filter :control_access, :get_time_zone
end
layout 'trebuchet'
helper :trebuchet
def index
@features = Trebuchet::Feature.all
@features.sort! {|x,y| x.name.downcase <=> y.name.downcase }
@dismantled_features = Trebuchet::Feature.dismantled
@dismantled_features.sort! {|x,y| x.name.downcase <=> y.name.downcase }
respond_to do |wants|
wants.html # index.html.erb
wants.json { render :json => @features.map(&:export) }
end
end
def timeline
@history = []
Trebuchet::Feature.all.each do |f|
f.history.each do |timestamp, strategy|
@history << {
:feature_name => f.name,
:timestamp => timestamp,
:strategy => strategy
}
end
end
@history = @history.sort_by { |h| h[:timestamp] }
@history.reverse!
respond_to do |wants|
wants.html # index.html.erb
wants.json do
json_history = @history.map do |history|
history.tap { |h| h[:strategy] = h[:strategy].export }
end
render :json => json_history
end
end
end
private
def control_access
allowed = if Trebuchet.admin_view.is_a?(Proc)
begin
instance_eval &(Trebuchet.admin_view)
rescue
false
end
else
!!Trebuchet.admin_view
end
raise ActionController::RoutingError.new('Not Found') unless allowed
end
def get_time_zone
@zone = if Trebuchet.time_zone
if Trebuchet.time_zone.is_a?(Proc)
Trebuchet.time_zone.call
elsif Trebuchet.time_zone.is_a?(String)
Trebuchet.time_zone
else
nil
end
end
@zone = ActiveSupport::TimeZone.new(@zone || 'UTC')
end
end
end
================================================
FILE: app/helpers/trebuchet_helper.rb
================================================
module TrebuchetHelper
def feature(feature)
"<dd>#{feature.name}</dd>
<dt><ul>#{strategy feature.strategy}</ul></dt>"
end
def strategy(strategy)
html = case strategy.name
when :multiple
strategy.strategies.map { |s| strategy s }.join().html_safe
else
strategy.to_s
end
strategy.name == :multiple ? html : content_tag(:li, html)
end
def trebuchet_css
filename = File.expand_path(File.dirname(__FILE__) + "/../views/trebuchet_rails/trebuchet.css")
return IO.read(filename)
end
end
================================================
FILE: app/views/layouts/trebuchet.html.erb
================================================
<!DOCTYPE html>
<html>
<head>
<title>Trebuchet</title>
<style type="text/css">
<%= trebuchet_css %>
</style>
</head>
<body>
<%= yield %>
</body>
</html>
================================================
FILE: app/views/trebuchet_rails/features/index.html.erb
================================================
<div id="trebuchet">
<h1>Trebuchet Admin</h1>
<table class="treb-features">
<thead>
<tr>
<th>Feature Name</th>
<th>Launch Strategy</th>
<th>Strategy History</th>
</tr>
</thead>
<tbody>
<% @features.each do |feature| %>
<%- has_history = feature.history.length > 0 %>
<tr>
<td><%= feature.name %></td>
<td><%= strategy feature.strategy %></td>
<% if has_history %>
<td>
<a class="history-toggle" href="#">History</a>
</td>
<% end %>
</tr>
<% if has_history %>
<tr>
<td class="unstyled collapsed treb-features-history-wrapper" colspan="2">
<table class="treb-features treb-features-history well">
<% feature.history.each do |time, strategy| %>
<tr><td><%= @zone.at(time.to_i).strftime("%x %X %Z") %><br /><%= strategy strategy %></td></tr>
<% end %>
</table>
</td>
</tr>
<% end %>
<% end %>
</tbody>
</table>
<br />
<br />
<table class="treb-features">
<thead>
<tr>
<th>Dismantled Features</th>
<th>Strategy History</th>
</tr>
</thead>
<tbody>
<% @dismantled_features.each do |feature| %>
<%- has_history = feature.history.length > 0 %>
<tr>
<td style="text-decoration: line-through"><%= feature.name %></td>
<% if has_history %>
<td>
<a class="history-toggle" href="#">History</a>
</td>
<% end %>
</tr>
<% if has_history %>
<tr>
<td class="unstyled collapsed treb-features-history-wrapper" colspan="1">
<table class="treb-features treb-features-history well">
<% feature.history.each do |time, strategy| %>
<tr><td><%= @zone.at(time.to_i).strftime("%x %X %Z") %><br /><%= strategy strategy %></td></tr>
<% end %>
</table>
</td>
</tr>
<% end %>
<% end %>
</tbody>
</table>
</div>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.0/jquery.min.js"></script>
<script>
(function(undefined) {
var trebFeatures = $('.treb-features');
trebFeatures.on('click', '.history-toggle', function(e) {
var $target = $(e.target);
$target.parents('tr').next().
find('.treb-features-history-wrapper').toggleClass('collapsed');
e.preventDefault();
});
})();
</script>
================================================
FILE: app/views/trebuchet_rails/features/timeline.html.erb
================================================
<div id="trebuchet">
<h1>Trebuchet Timeline</h1>
<table class="treb-features">
<thead>
<tr>
<th style="min-width: 15em">Timestamp</th>
<th>Feature Name</th>
<th>Launch Strategy</th>
</tr>
</thead>
<tbody>
<% @history.each do |h| %>
<tr>
<td><%= @zone.at(h[:timestamp].to_i).strftime("%x %X %Z") %></td>
<td><%= h[:feature_name] %></td>
<td><%= strategy h[:strategy] %></td>
</tr>
<% end %>
</tbody>
</table>
</div>
================================================
FILE: app/views/trebuchet_rails/trebuchet.css
================================================
#trebuchet {
font-family: HelveticaNeue, 'Helvetica Neue', HelveticaNeueRoman, HelveticaNeue-Roman, 'Helvetica Neue Roman', TeXGyreHerosRegular, Helvetica, Tahoma, Geneva, Arial, sans-serif;
}
.well {
-webkit-box-shadow: inset 0 0 15px rgba(0, 0, 0, 0.2), 0 0 0 black;
box-shadow: inset 0 0 15px rgba(0, 0, 0, 0.2), 0 0 0 black;
}
.collapsed {
display: none;
}
.treb-features {
border-collapse: collapse;
border: 1px solid #c9c9c9;
border-top-color: #000000;
}
.treb-features-history {
border: none;
font-size: 0.8em;
width: 100%;
}
.treb-features thead {
background: -webkit-gradient(linear, center top, center bottom, from(#575a5b), to(#393c3d));
background: -moz-linear-gradient(top, #575a5b, #393c3d);
color: #ffffff;
text-shadow: -1px -1px 1px black;
}
.treb-features td,
.treb-features th {
border: 1px solid #ccc;
padding: 1em 1.5em;
text-align: left;
}
.treb-features-history tr:first-child td {
border-top: none;
}
.treb-features-history tr:last-child td {
border-bottom: none;
}
.treb-features td {
background-color: #f5f5f5;
}
.treb-features-history td {
background: transparent;
border-left: none;
border-right: none;
}
.treb-features th {
border-left-color: #525657;
border-right-color: #74787a;
font-weight: normal;
}
.treb-features .unstyled {
padding: 0;
}
================================================
FILE: config/routes.rb
================================================
# TrebuchetRails::Engine.routes do
routes_block = lambda do
scope "trebuchet", :module => "trebuchet_rails" do
get '/' => "features#index"
get 'timeline' => "features#timeline"
end
end
if Rails::VERSION::MAJOR == 3
case Rails::VERSION::MINOR
when 0
Rails.application.routes.draw &routes_block
when 1
Rails.application.routes.prepend &routes_block
when 2
Rails.application.routes &routes_block
end
end
================================================
FILE: init.rb
================================================
require 'trebuchet'
# FIXME: happens too early
# def set_trebuchet_namespace(app_name)
# if Trebuchet.backend.respond_to?(:namespace=)
# Trebuchet.backend.namespace = "trebuchet-#{app_name}/"
# end
# end
if defined? Rails
Trebuchet.use_with_rails!
if Rails.respond_to?(:version) && Rails.version =~ /^3/
# Rails 3.x
# no other setup needed
else
# Rails 2.x
load_paths.each do |path|
ActiveSupport::Dependencies.load_once_paths.delete(path)
end if config.environment == 'development'
# set_trebuchet_namespace "#{Rails.root.basename}-#{Rails.env}"
end
end
================================================
FILE: lib/trebuchet/action_controller.rb
================================================
require 'trebuchet/action_controller_filter'
module Trebuchet::ActionController
def self.included(base)
base.helper_method :trebuchet
base.class_eval do
around_filter Trebuchet::ActionControllerFilter
end
end
def trebuchet
Trebuchet.current
end
end
================================================
FILE: lib/trebuchet/action_controller_filter.rb
================================================
class Trebuchet::ActionControllerFilter
def self.before(controller)
Trebuchet.initialize_logs
if Trebuchet.backend.respond_to?(:refresh)
Trebuchet.backend.refresh
end
Trebuchet.current_block = Proc.new {
Trebuchet.new(controller.send(:current_user), controller.request)
}
end
def self.after(controller)
Trebuchet.current_block = nil
Trebuchet.reset_current! # very important
end
end
================================================
FILE: lib/trebuchet/backend/disabled.rb
================================================
# This backend stores nothing and returns empty/false data (launch? will always be false)
# It can be used to disable all Trebuchet features (especially if Trebuchet fails to connect to it's normal data store)
class Trebuchet::Backend::Disabled
def initialize(*args)
end
def get_strategy(feature_name)
[:default]
end
def set_strategy(feature, strategy, options = nil)
false
end
def append_strategy(feature, strategy, options = nil)
false
end
def get_feature_names
[]
end
end
================================================
FILE: lib/trebuchet/backend/memcached.rb
================================================
require 'memcache'
class Trebuchet::Backend::Memcached
attr_accessor :namespace
def initialize(*args)
@memcache = MemCache.new(*args)
@namespace = 'trebuchet/'
end
def get_strategy(feature_name)
@memcache.get(key(feature_name))
end
def set_strategy(feature, strategy, options = nil)
@memcache.set(key(feature), [strategy, options])
end
def append_strategy(feature, strategy, options = nil)
@memcache.set(key(feature), get_strategy(feature) + [strategy, options])
end
def get_feature_names
[] # TODO: store all key names
end
private
def key(feature_name)
"#{namespace}#{feature_name}"
end
end
================================================
FILE: lib/trebuchet/backend/memory.rb
================================================
class Trebuchet::Backend::Memory
def initialize(*args)
@hash = {}
@archived = []
end
def get_strategy(feature_name)
@hash.fetch(feature_name, nil) || []
end
def set_strategy(feature, strategy, options = nil)
@archived.delete(feature)
@hash.store(feature, [strategy, options])
end
def append_strategy(feature, strategy, options = nil)
@archived.delete(feature)
strategies = get_strategy(feature) || []
if i = strategies.index(strategy)
strategies.delete_at(i) # remove strategy_name
strategies.delete_at(i) # remove options
end
strategies += [strategy, options]
@hash.store(feature, strategies)
end
def remove_strategy(feature)
@hash.delete(feature)
end
def get_feature_names
@hash.keys
end
def remove_feature(feature)
@hash.delete(feature)
@archived << feature
@archived.uniq!
end
def get_archived_feature_names
@archived
end
end
================================================
FILE: lib/trebuchet/backend/redis.rb
================================================
require 'redis' unless defined?(Redis)
require 'json'
class Trebuchet::Backend::Redis
attr_accessor :namespace
def initialize(*args)
@namespace = 'trebuchet/'
begin
if args.first.is_a?(Hash) && (client = args.first[:client]) && (client.is_a?(Redis) || client.is_a?(MockRedis))
# ignore other args and use provided Redis connection
@options = args.first
@redis = args.first[:client]
else
@redis = Redis.new(*args)
end
unless @options && @options[:skip_check]
# raise error if not connectedUncaught ReferenceError: google is not defined
@redis.exists(feature_names_key) # @redis.info is slow and @redis.client.connected? is NOT reliable
end
rescue Exception => e
raise Trebuchet::BackendInitializationError, e.message
end
end
def get_strategy(feature_name)
return nil unless h = @redis.hgetall(feature_key(feature_name))
unpack_strategy(h)
end
def unpack_strategy(options)
return nil unless options.is_a?(Hash)
[].tap do |a|
options.each do |k, v|
begin
key = k.to_sym
value = JSON.load(v).first # unpack from array
a << key
a << value
rescue
# if it can't parse the JSON, skip it
end
end
end
end
def set_strategy(feature_name, strategy, options = nil)
remove_strategy(feature_name)
append_strategy(feature_name, strategy, options)
update_sentinel
end
def append_strategy(feature_name, strategy, options = nil)
@redis.srem(archived_feature_names_key, feature_name)
@redis.hset(feature_key(feature_name), strategy, [options].to_json) # have to put options in container for json
@redis.sadd(feature_names_key, feature_name)
store_history(feature_name)
update_sentinel
end
def remove_strategy(feature_name)
@redis.del(feature_key(feature_name))
update_sentinel
end
def get_feature_names
@redis.smembers(feature_names_key)
end
def get_archived_feature_names
@redis.smembers(archived_feature_names_key)
end
def remove_feature(feature_name)
@redis.del(feature_key(feature_name))
@redis.srem(feature_names_key, feature_name)
@redis.sadd(archived_feature_names_key, feature_name)
update_sentinel
end
def store_history(feature_name)
timestamp = Time.now.to_i
h = @redis.hgetall(feature_key(feature_name))
@redis.hmset(feature_history_key(feature_name, timestamp), *h.to_a.flatten)
@redis.sadd(feature_history_key(feature_name), timestamp) # subtle
end
def get_history(feature_name)
[].tap do |history|
@redis.smembers(feature_history_key(feature_name)).sort.each do |timestamp|
h = @redis.hgetall(feature_history_key(feature_name, timestamp))
history << [timestamp.to_i, unpack_strategy(h)]
end
end
end
def get_all_history(include_archived = false)
history = []
features = @redis.smembers(feature_names_key)
features += @redis.smembers(archived_feature_names_key) if include_archived
result = @redis.pipelined do
features.each do |feature_name|
@redis.smembers(feature_history_key(feature_name))
end
end
features.zip(result).each do |feature_name, timestamps|
timestamps.each do |timestamp|
history << [timestamp.to_i, feature_name]
end
end
# sort in reverse timestamp order
history.sort! { |x,y| y.first <=> x.first }
end
def update_sentinel
@redis.set(sentinel_key, Time.now.to_i)
end
def get_sentinel
@redis.get(sentinel_key) || Time.now.to_i
end
private
def archived_feature_names_key
"#{namespace}archived-feature-names"
end
def feature_names_key
"#{namespace}feature-names"
end
def feature_key(feature_name)
"#{namespace}features/#{feature_name}"
end
def feature_history_key(feature_name, timestamp = nil)
key = "#{namespace}feature-history/#{feature_name}"
key = "#{key}/#{timestamp}" if timestamp
key
end
def sentinel_key
"#{namespace}last_updated"
end
end
================================================
FILE: lib/trebuchet/backend/redis_cached.rb
================================================
require 'trebuchet/backend/redis'
class Trebuchet::Backend::RedisCached < Trebuchet::Backend::Redis
# cache strategies in memory until clear_cached_strategies is called
def get_strategy(feature_name)
if cached_strategies.has_key?(feature_name)
# use cached if available (even if value is nil)
cached_strategies[feature_name]
else
# or call Trebuchet::Backend::Redis#get_strategy
# which will fetch from Redis and unpack json
# and then cache it for next time
cache_strategy feature_name, super(feature_name)
end
end
def append_strategy(feature_name, strategy, options = nil)
# though we can't clear the strategy for all active instances
# this will clear the cache in the console environment to show current settings
clear_cached_strategies
super(feature_name, strategy, options)
end
def cache_strategy(feature_name, strategy)
cached_strategies[feature_name] = strategy
end
def cached_strategies
@cached_strategies ||= Hash.new
end
def cache_cleared_at
@cache_cleared_at ||= Time.now
end
def clear_cached_strategies
@cache_cleared_at = Time.now
@cached_strategies = nil
end
def refresh
clear_cached_strategies if Time.now > cache_cleared_at + 60.seconds
end
end
================================================
FILE: lib/trebuchet/backend/redis_hammerspaced.rb
================================================
require 'trebuchet/backend/redis'
require 'json'
class Trebuchet::Backend::RedisHammerspaced < Trebuchet::Backend::Redis
# This class will rely on a cron job to sync all trebuchet features
# to local hammerspace thus this class never directly updates hammerspace
# We also cache in memory the features and rely on before_filter
# to lazily invalidate local cache
attr_accessor :namespace
def initialize(*args)
# args.first must be a hash
super(*args)
@hammerspace = args.first[:hammerspace]
end
def get_strategy(feature_name)
if cached_strategies.has_key?(feature_name)
# use cached if available (even if value is nil)
cached_strategies[feature_name]
else
# call to hammerspace
cache_strategy feature_name, get_strategy_hammerspace(feature_name)
end
end
def get_strategy_hammerspace(feature_name)
# Read from hammerspace
h = @hammerspace[feature_key(feature_name)]
return nil unless h
# h will be a string, we need to convert it back to Hash
begin
h = JSON.load(h)
rescue
return nil
end
unpack_strategy_hammerspace(h)
end
def unpack_strategy_hammerspace(options)
# We don't need to further convert values
# because it's already taken care of
# by the refresh cron job
# assumption here is that v will be an array and we
# are using the first element for now
# This makes the format compatible with redis backend
return nil unless options.is_a?(Hash)
[].tap do |a|
options.each do |k, v|
key = k.to_sym
a << key
a << v.first
end
end
end
def get_feature_names
# Read from hammerspace
return [] unless @hammerspace.has_key?(feature_names_key)
JSON.load(@hammerspace[feature_names_key])
end
def append_strategy(feature_name, strategy, options = nil)
# though we can't clear the strategy for all active instances
# this will clear the cache in the console environment to show current settings
clear_cached_strategies
super(feature_name, strategy, options)
end
def cache_strategy(feature_name, strategy)
cached_strategies[feature_name] = strategy
end
def cached_strategies
@cached_strategies ||= Hash.new
end
def clear_cached_strategies
@cached_strategies = nil
end
def refresh
# We close and reopen hammerspace to see if we need to invalidate local cache
uid = @hammerspace.uid
@hammerspace.close
if @hammerspace.uid != uid
clear_cached_strategies
end
end
def update_hammerspace(forced = false)
last_updated = get_sentinel
return if !forced && last_updated == @hammerspace[sentinel_key]
feature_names = @redis.smembers(feature_names_key)
features = @redis.pipelined do
feature_names.each do |feature_name|
@redis.hgetall(feature_key(feature_name))
end
end
hash = generate_hammerspace_hash(feature_names, features, last_updated)
@hammerspace.replace(hash)
@hammerspace.close
clear_cached_strategies
end
# feature_names is an array of strings
# features is an array of strategies
# Each strategy is of form ["<key1>", "<value1>", "<key2>", "<value2>"...]
# We need to decode the values because they are in string form (not actual hash)
def generate_hammerspace_hash(feature_names, features, last_updated)
hash = {
sentinel_key => last_updated.to_s,
feature_names_key => feature_names.to_json,
}
feature_names.zip(features) do |feature_name, feature|
h = {}
feature.each_slice(2) {|k,v| h[k]=JSON.load(v)}
hash[feature_key(feature_name)] = h.to_json
end
hash
end
end
================================================
FILE: lib/trebuchet/backend.rb
================================================
module Trebuchet::Backend
def self.lookup(name)
# From ActiveSupport::Inflector.camelize
const_name = name.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
if const_defined?(const_name)
const_get(const_name)
else
raise ArgumentError.new("Unknown backend type #{name}")
end
end
end
================================================
FILE: lib/trebuchet/error.rb
================================================
class Trebuchet::Error < StandardError ; end
class Trebuchet::BackendInitializationError < Trebuchet::Error ; end
class Trebuchet::BackendError < Trebuchet::Error ; end
================================================
FILE: lib/trebuchet/feature/stubbing.rb
================================================
class Trebuchet
class Feature
module Stubbing
def stub(state)
self.class.stubbed_features[name] = state
end
def stubbed?
!!self.class.stubbed_features[name]
end
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def dismantle_stubs
@stubbed_features = nil
end
def stubbed_features
@stubbed_features ||= {}
end
end
end
end
end
================================================
FILE: lib/trebuchet/feature.rb
================================================
require 'trebuchet/feature/stubbing'
class Trebuchet::Feature
include Stubbing
@@deprecated_strategies_enabled = true
@@features = {}
attr_accessor :name
def initialize(name)
@name = name
end
def self.find(name)
feature = @@features[name]
if !feature
feature = new(name)
@@features[name] = feature
end
feature.reset
feature
end
def reset
@chained = false
end
def self.all
Trebuchet.backend.get_feature_names.map{|name| new(name)}
end
def self.dismantled
Trebuchet.backend.get_archived_feature_names.map{|name| new(name)}
end
def self.exist?(name)
!!all.detect{|feature| feature.name == name }
end
# Runs the block with deprecated features enabled so that various methods
# do not raise exceptions. This was added to allow specs to test
# deprecated features. Not thread safe.
def self.with_deprecated_strategies_enabled(value=true, &block)
original_value = @@deprecated_strategies_enabled
begin
@@deprecated_strategies_enabled = value
block.call()
ensure
@@deprecated_strategies_enabled = original_value
end
end
def strategy
Trebuchet::Strategy.for_feature(self)
end
def valid?
strategy.name != :invalid
end
def launch_at?(user, request = nil)
# Store strategy so that only one call to the backend is needed.
s = strategy
(!s.needs_user? || !user.nil?) && s.launch_at?(user, request)
end
def aim(strategy_name, options = nil)
if !@@deprecated_strategies_enabled &&
Trebuchet::Strategy.deprecated_strategy_names.include?(strategy_name)
raise "The #{strategy_name} strategy is deprecated."
end
if chained?
Trebuchet.backend.append_strategy(self.name, strategy_name, options)
else
Trebuchet.backend.set_strategy(self.name, strategy_name, options)
end
@chained = true
self
end
# add/edit just one strategy without affecting other chained strategies
def adjust(strategy_name, options = nil)
Trebuchet.backend.append_strategy(self.name, strategy_name, options)
self
end
# add to the options of a strategy (if it is an integer, hash or array)
def augment(strategy_name, new_options)
# get old options if any
strategy_array = Trebuchet.backend.get_strategy(self.name)
i = strategy_array.index(strategy_name)
old_options = i ? strategy_array[i+1] : nil
# augment them carefully
options = if old_options == nil
new_options
elsif old_options.is_a?(Array) && new_options.is_a?(Array)
old_options + new_options
elsif old_options.is_a?(Hash) && new_options.is_a?(Hash)
old_options.merge(new_options)
elsif old_options.is_a?(Numeric) && new_options.is_a?(Numeric)
old_options + new_options
else # otherwise, change nothing
old_options
end
# adjust that strategy
self.adjust(strategy_name, options)
end
def dismantle
Trebuchet.backend.remove_feature(self.name)
end
# add comments for a feature, as a place to hold change logs for example, to supported backends
def add_comment(comment)
if Trebuchet.backend.respond_to?(:add_comment)
Trebuchet.backend.add_comment(self.name, comment)
end
end
# Retrieve the expiration date of the feature.
# Return nil if the feature does not have an expiration date.
def expiration_date
return unless Trebuchet.backend.respond_to?(:expiration_date)
Trebuchet.backend.expiration_date(self.name)
end
# Set the expiration date of the feature.
def set_expiration_date(expiration_date)
return unless Trebuchet.backend.respond_to?(:set_expiration_date)
Trebuchet.backend.set_expiration_date(self.name, expiration_date)
end
def history
return [] unless Trebuchet.backend.respond_to?(:get_history)
Trebuchet.backend.get_history(self.name).map do |row|
[Time.at(row.first), Trebuchet::Strategy.find(*row.last)]
end
end
def feature_id
begin
@feature_id ||= Trebuchet::SHA1.hexdigest(@name).to_i(16)
rescue
return 0
end
end
def as_json(options = {})
{:name => @name, :strategy => strategy.export}
end
def to_s
str = "name: \"#{@name}\", "
str << "#{strategy.name == :multiple ? 'strategies' : 'strategy'}: #{strategy}"
end
def inspect
"#<#{self.class.name} #{self}>"
end
def export
{:feature_name => name, :strategy => strategy.export}
end
private
def chained?
@chained
end
end
================================================
FILE: lib/trebuchet/state.rb
================================================
# Represents the internal, global and thread-unsafe state of Trebuchet
Trebuchet::State = Struct.new(
:visitor_id, :current, :current_block,
:logs, :admin_view, :admin_edit, :time_zone,
:author
)
================================================
FILE: lib/trebuchet/strategy/base.rb
================================================
require 'digest/sha1'
class Trebuchet::Strategy::Base
attr_accessor :feature
def name
self.class.strategy_name
end
def feature_id
feature.feature_id
end
def needs_user?
true
end
def self.strategy_name
Trebuchet::Strategy.name_for_class(self)
end
def as_json(options = {})
excluded = [:feature, :block]
{:name => name}.tap do |h|
instance_variables.map do |v|
key = v.to_s.gsub('@','').to_sym
h[key] = instance_variable_get(v) unless excluded.include?(key)
end
end
end
def inspect
self.to_s
end
def export(options = nil)
{:name => self.name, :options => options}
end
end
================================================
FILE: lib/trebuchet/strategy/custom.rb
================================================
class Trebuchet::Strategy::Custom < Trebuchet::Strategy::Base
attr_reader :options, :custom_name
@@custom_strategies = {}
def initialize(name, options = nil)
@custom_name = name
@options = options
@block = @@custom_strategies[name]
end
def launch_at?(user, request = nil)
!!(options ? @block.call(user, options) : @block.call(user))
end
def self.define(name, block)
@@custom_strategies[name] = block
end
def self.exists?(name)
@@custom_strategies.has_key?(name)
end
def needs_user?
false
# re-enable after adding { |options, user, request| }
# if block = @@custom_strategies[custom_name]
# block.arity > 0
# else
# false
# end
end
def as_json(options = {})
{:custom_name => @custom_name, :options => @options}
end
def to_s
"#{custom_name} (custom) #{options.inspect if options}"
end
def export
super as_json
end
end
================================================
FILE: lib/trebuchet/strategy/custom_request_aware.rb
================================================
class Trebuchet::Strategy::CustomRequestAware < Trebuchet::Strategy::Custom
@@custom_request_aware_strategies = {}
def initialize(name, options = nil)
@custom_name = name
@options = options
@block = @@custom_request_aware_strategies[name]
end
def self.define(name, block)
@@custom_request_aware_strategies[name] = block
end
def self.exists?(name)
@@custom_request_aware_strategies.has_key?(name)
end
def launch_at?(user, request = nil)
request ||= {}
!!(options ? @block.call(user, request, options) : @block.call(user, request))
end
def to_s
"#{custom_name} (custom_request_aware) #{options.inspect if options}"
end
end
================================================
FILE: lib/trebuchet/strategy/default.rb
================================================
require 'singleton'
# Default is to not launch the feature to anyone
class Trebuchet::Strategy::Default < Trebuchet::Strategy::Base
include Singleton
def initialize(options = nil)
# ignore options
end
def name
:default
end
def launch_at?(user, request = nil)
false
end
def needs_user?
false
end
def to_s
"not launched (default)"
end
end
================================================
FILE: lib/trebuchet/strategy/everyone.rb
================================================
require 'singleton'
# Everyone is to launch the feature to everyone
class Trebuchet::Strategy::Everyone < Trebuchet::Strategy::Base
include Singleton
def initialize(options = nil)
# ignore options
end
def name
:everyone
end
def launch_at?(user, request = nil)
true
end
def needs_user?
false
end
def to_s
"launched to everyone"
end
end
================================================
FILE: lib/trebuchet/strategy/experiment.rb
================================================
# require 'digest/sha1'
class Trebuchet::Strategy::Experiment < Trebuchet::Strategy::Base
include Trebuchet::Strategy::Experimentable
def initialize(options = {})
initialize_experiment(options)
end
def launch_at?(user, request = nil)
return false unless user && user.id
value_in_bucket?(user.id)
end
# def to_s from experimentable
# def export from experimentable
end
================================================
FILE: lib/trebuchet/strategy/hostname.rb
================================================
class Trebuchet::Strategy::Hostname < Trebuchet::Strategy::Base
attr_reader :hostnames
def initialize(hostnames)
@hostnames = if hostnames.is_a?(Array)
hostnames
else
[hostnames]
end
end
def launch_at?(user, request = nil)
return false if request.nil?
self.hostnames.include?(request.host)
end
def needs_user?
false
end
def to_s
"hostnames (#{hostnames.empty? ? 'none' : hostnames.join(', ')})"
end
def export
super @hostnames
end
end
================================================
FILE: lib/trebuchet/strategy/invalid.rb
================================================
# Default is to not launch the feature to anyone
class Trebuchet::Strategy::Invalid < Trebuchet::Strategy::Base
attr_reader :invalid_name, :options
def initialize(name, options = nil)
@invalid_name = name
@options = options
end
def name
:invalid
end
def launch_at?(user, request = nil)
false
end
def needs_user?
false
end
def to_s
"#{invalid_name} (invalid) #{options.inspect if options}"
end
end
================================================
FILE: lib/trebuchet/strategy/logic_and.rb
================================================
require 'trebuchet/strategy/logic_base'
class Trebuchet::Strategy::LogicAnd < Trebuchet::Strategy::LogicBase
def launch_at?(user, request = nil)
@strategies
.all? { |s| (!s.needs_user? || !user.nil?) && s.launch_at?(user, request) }
end
end
================================================
FILE: lib/trebuchet/strategy/logic_base.rb
================================================
class Trebuchet::Strategy::LogicBase < Trebuchet::Strategy::Base
attr_reader :strategies
attr_reader :options
def initialize(options = {})
@options = options
@strategies = []
options.each do |strategy_name, strategy_options|
@strategies << Trebuchet::Strategy.find(strategy_name.to_sym, strategy_options)
end
end
# Override feature setter so that @feature gets set on @strategies as well
def feature=(f)
@feature = f
@strategies.each { |s| s.feature = f }
end
def launch_at?(user, request = nil)
false # To be overriden in implementation classes.
end
def needs_user?
false
end
end
================================================
FILE: lib/trebuchet/strategy/logic_not.rb
================================================
require 'trebuchet/strategy/logic_base'
class Trebuchet::Strategy::LogicNot < Trebuchet::Strategy::LogicBase
def launch_at?(user, request = nil)
@strategies
.none? { |s| (!s.needs_user? || !user.nil?) && s.launch_at?(user, request) }
end
end
================================================
FILE: lib/trebuchet/strategy/logic_or.rb
================================================
require 'trebuchet/strategy/logic_base'
class Trebuchet::Strategy::LogicOr < Trebuchet::Strategy::LogicBase
def launch_at?(user, request = nil)
@strategies
.any? { |s| (!s.needs_user? || !user.nil?) && s.launch_at?(user, request) }
end
end
================================================
FILE: lib/trebuchet/strategy/multiple.rb
================================================
class Trebuchet::Strategy::Multiple < Trebuchet::Strategy::Base
attr_reader :strategies
def initialize(args)
@strategies = []
args.each_slice(2) do |pair|
@strategies << Trebuchet::Strategy.find(*pair)
end
end
# override setter so that @feature gets set on @strategies as well
def feature=(f)
@feature = f
@strategies.each {|s| s.feature = f}
end
def launch_at?(user, request = nil)
!!(strategies.select{|s| !user.nil? || !s.needs_user?}.find { |s| s.launch_at?(user, request) })
# !!(strategies.find { |s| s.launch_at?(user, request) })
end
def as_json(options = {})
@strategies
end
def needs_user?
false # assume some of the strategies may not need user
# could change this so it calls only the strategies that don't need a user when none is present
# strategies.any? { |s| s.needs_user? }
end
def export
super :strategies => strategies.map(&:export)
end
end
================================================
FILE: lib/trebuchet/strategy/nobody.rb
================================================
require 'singleton'
# Nobody is to launch the feature to nobody
class Trebuchet::Strategy::Nobody < Trebuchet::Strategy::Base
include Singleton
def initialize(options = nil)
# ignore options
end
def name
:nobody
end
def launch_at?(user, request = nil)
false
end
def needs_user?
false
end
def to_s
"launched to nobody"
end
end
================================================
FILE: lib/trebuchet/strategy/per_denomination.rb
================================================
class Trebuchet::Strategy::PerDenomination < Trebuchet::Strategy::Base
include Trebuchet::Strategy::PerDenominationable
def set_range_from_options(options = {})
numerator = options['numerator'] || options[:numerator] || 0
denominator = options['denominator'] || options[:denominator] || 0
super(numerator: numerator, denominator: denominator)
end
def launch_at?(user, request = nil)
return false unless user && user.id
value_in_range?(user.id.to_i)
end
# def to_s from PerDenominationable
# def export from PerDenominationable
end
================================================
FILE: lib/trebuchet/strategy/percent.rb
================================================
class Trebuchet::Strategy::Percent < Trebuchet::Strategy::Base
include Trebuchet::Strategy::Percentable
def initialize(options)
set_range_from_options(options)
end
def launch_at?(user, request = nil)
return false unless user && user.id
value_in_range?(user.id.to_i)
end
# def to_s from percentable
# def export from percentable
end
================================================
FILE: lib/trebuchet/strategy/percent_deprecated.rb
================================================
class Trebuchet::Strategy::PercentDeprecated < Trebuchet::Strategy::Base
include Trebuchet::Strategy::PercentableDeprecated
def initialize(options)
set_range_from_options(options)
end
def launch_at?(user, request = nil)
return false unless user && user.id
value_in_range?(user.id.to_i)
end
# def to_s from percentable
# def export from percentable
end
================================================
FILE: lib/trebuchet/strategy/stub.rb
================================================
class Trebuchet
module Strategy
class Stub < Trebuchet::Strategy::Base
attr_reader :state
def initialize(state)
@state = state
end
def launch_at?(user, request = nil)
state == :launched
end
def needs_user?
false
end
def to_s
"stub (#{state}}"
end
def export
super state
end
end
end
end
================================================
FILE: lib/trebuchet/strategy/user_id.rb
================================================
class Trebuchet::Strategy::UserId < Trebuchet::Strategy::Base
attr_reader :user_ids
def initialize(user_ids)
user_ids = Array(user_ids).flatten
@user_ids = Set.new(user_ids)
end
def launch_at?(user, request = nil)
@user_ids.include?(user.id)
end
def to_s
"user ids (#{user_ids.empty? ? 'none' : user_ids.to_a.join(', ')})"
end
def export
super @user_ids.to_a
end
end
================================================
FILE: lib/trebuchet/strategy/visitor_experiment.rb
================================================
class Trebuchet::Strategy::VisitorExperiment < Trebuchet::Strategy::Base
include Trebuchet::Strategy::Experimentable
def initialize(options = {})
initialize_experiment(options)
end
def launch_at?(user, request = nil)
if Trebuchet.visitor_id.respond_to?(:call)
visitor_id = Trebuchet.visitor_id.call(request)
else
visitor_id = nil
end
return false if visitor_id.nil?
value_in_bucket?(visitor_id)
end
def needs_user?
false
end
# def to_s from experimentable
# def export from experimentable
end
================================================
FILE: lib/trebuchet/strategy/visitor_percent.rb
================================================
class Trebuchet::Strategy::VisitorPercent < Trebuchet::Strategy::Base
include Trebuchet::Strategy::Percentable
def initialize(options)
set_range_from_options(options)
end
def launch_at?(user, request = nil)
return false if request.nil?
if Trebuchet.visitor_id.respond_to?(:call)
visitor_id = Trebuchet.visitor_id.call(request)
else
visitor_id = nil
end
return false if visitor_id.nil?
value_in_range?(visitor_id.to_i)
end
def needs_user?
false
end
# def to_s from percentable
end
================================================
FILE: lib/trebuchet/strategy/visitor_percent_deprecated.rb
================================================
class Trebuchet::Strategy::VisitorPercentDeprecated < Trebuchet::Strategy::Base
include Trebuchet::Strategy::PercentableDeprecated
def initialize(options)
set_range_from_options(options)
end
def launch_at?(user, request = nil)
return false if request.nil?
if Trebuchet.visitor_id.respond_to?(:call)
visitor_id = Trebuchet.visitor_id.call(request)
else
visitor_id = nil
end
return false if visitor_id.nil?
value_in_range?(visitor_id.to_i)
end
def needs_user?
false
end
# def to_s from percentable
end
================================================
FILE: lib/trebuchet/strategy.rb
================================================
require 'digest/sha1'
module Trebuchet::Strategy
def self.for_feature(feature)
stub_state = Trebuchet::Feature.stubbed_features[feature.name]
if stub_state
Stub.new(stub_state)
else
strategy_args = Trebuchet.backend.get_strategy(feature.name)
find(*strategy_args).tap {|s| s.feature = feature }
end
end
def self.find(*args)
strategy_name, options = args
if args.size > 2
Multiple.new(args)
elsif strategy_name.nil? || strategy_name == :default
# Strategy hasn't been defined yet
Default.instance
elsif strategy_name == :everyone
Everyone.instance
elsif strategy_name == :nobody
Nobody.instance
elsif CustomRequestAware.exists?(strategy_name)
CustomRequestAware.new(strategy_name, options)
elsif Custom.exists?(strategy_name)
Custom.new(strategy_name, options)
elsif klass = class_for_name(strategy_name)
# percent, users
klass.new(options)
else
Invalid.new(strategy_name, options)
end
end
# The stub strategy purposely left out of this list as it should be
# accessible via the testing interface only and not externally.
def self.name_class_map
[
[:visitor_percent_deprecated, VisitorPercentDeprecated],
[:percent_deprecated, PercentDeprecated],
[:percent, Percent],
[:per_denomination, PerDenomination],
[:users, UserId],
[:default, Default],
[:everyone, Everyone],
[:nobody, Nobody],
[:custom, Custom],
[:multiple, Multiple],
[:experiment, Experiment],
[:visitor_percent, VisitorPercent],
[:hostname, Hostname],
[:visitor_experiment, VisitorExperiment],
[:logic_and, LogicAnd],
[:logic_or, LogicOr],
[:logic_not, LogicNot],
]
end
def self.deprecated_strategy_names
[
:percent_deprecated,
:visitor_percent_deprecated
]
end
def self.class_for_name(name)
classes = Hash[name_class_map]
classes[name]
end
def self.name_for_class(klass)
names = Hash[name_class_map.map(&:reverse)]
names[klass]
end
module PerDenominationable
attr_reader :numerator, :denominator
def initialize(options)
set_range_from_options(options)
end
# must be called from initialize
def set_range_from_options(options)
raise ArgumentError, "Missing required input numerator" unless options[:numerator]
raise ArgumentError, "Missing required input denominator" unless options[:denominator]
@numerator = options[:numerator].to_i
@denominator = options[:denominator].to_i
raise ArgumentError, "Invalid denominator #{@denominator}" if @denominator <= 0
if @numerator > @denominator
raise ArgumentError, "Invalid numerator #{@numerator} > denominator #{@denominator}"
end
end
def value_in_range?(value)
bucket = Trebuchet::SHA1.hexdigest("#{@feature.name}|#{value}").to_i(16) % denominator
bucket < numerator
end
def to_s
"#{numerator} / #{denominator} of users"
end
def export
super({ numerator: numerator, denominator: denominator })
end
end
### Percentable module standardizes logic for percentage-based strategies
module Percentable
include PerDenominationable
alias_method :percentage, :numerator
# must be called from initialize
def set_range_from_options(options)
numerator =
if options == nil || options.is_a?(Numeric)
options.to_i
else
0
end
super(numerator: numerator, denominator: 100)
end
def to_s
kind = self.name == :visitor_percent ? "visitors" : "users"
percentage_str = "#{percentage}% of #{kind}"
"#{percentage_str}"
end
def export
super(percentage)
end
end
# This module is deprecated because the implementation is such that it's
# not possible to trust per-feature analysis if multiple features are
# using the PercentableDeprecated based strategies because the same
# visitors will tend to get the same features (even with the offset).
module PercentableDeprecated
def initialize(options)
set_range_from_options(options)
end
# must be called from initialize
def set_range_from_options(options)
if options == nil || options.is_a?(Numeric)
@from = 0
@to = options.to_i - 1
@style = :percentage
elsif options.is_a?(Hash) && (p = options['percentage'] || options[:percentage])
@from = 0
@to = p.to_i - 1
@style = :percentage
elsif options.is_a?(Hash)
@from = options['from'] || options[:from]
@to = options['to'] || options[:to]
@style = :range
else
@from = 0
@to = -1
end
end
def offset
if @style == :percentage
feature_id % 100
else
0
end
end
def percentage
return 0 unless @to.is_a?(Integer) && @from.is_a?(Integer)
return 0 if @to < 0
((@to - @from) + 100) % 100 + 1
end
# call from launch_at? and pass in user id or another integer
def value_in_range?(value)
return false unless @from && @to
return false if @from.to_i < 0 || @to.to_i < 0
return false if value == nil || !value.is_a?(Numeric)
cutoff = percentage
value = ((value - @from) + 200 - offset) % 100
!!(value < cutoff)
end
def offset_from
(@from + offset) % 100
end
def offset_to
(@to + offset) % 100
end
def to_s
kind = self.name == :visitor_percent ? "visitors" : "users"
percentage_str = "#{percentage}% of #{kind}"
range_str = if @to < 0
"nobody"
else
str = ''
str << "user id ending with " if kind != "visitors"
str << "#{offset_from.to_s.rjust(2, '0')}"
str << " to #{offset_to.to_s.rjust(2, '0')}" if @to != @from
str
end
@style == :range ? "#{range_str} (#{percentage_str})" : "#{percentage_str} (#{range_str})"
end
def export
if @style == :percentage
super :percentage => @to
else
super :from => @from, :to => @to
end
end
end
module Experimentable
attr_reader :bucket, :total_buckets, :experiment_name
def initialize_experiment(options)
options.keys.each {|k| options[k.to_sym] = options.delete(k)} # cheap symbolize_keys
@experiment_name = options[:name]
@bucket = [ options[:bucket] ].flatten # always treat as an array
@total_buckets = options[:total_buckets] || 5
end
def value_in_bucket?(value)
return false if value == nil || !value.is_a?(Numeric)
return false unless self.valid?
# must hash feature name and value together to ensure uniform distribution
b = Trebuchet::SHA1.hexdigest("experiment: #{@experiment_name.downcase} user: #{value}").to_i(16) % total_buckets
!!@bucket.include?(b + 1) # is user in this bucket?
end
def valid?
experiment_name && total_buckets > 0 && bucket.max <= total_buckets && (1..total_buckets).include?(bucket.min)
rescue
false
end
def type
"#{name == :experiment ? "user" : "visitor"} experiment"
end
def as_json(options = {})
{
:name => experiment_name,
:bucket => bucket,
:total_buckets => total_buckets,
:type => self.type
}
end
def to_s
str = "buckets (#{bucket.join(', ')}) of total: #{total_buckets}"
str << " for #{self.type} experiment: #{experiment_name}"
end
def export
super :name => experiment_name, :bucket => bucket, :total_buckets => total_buckets
end
def inspect
"#<#{self.class.name} #{self}>"
end
end
end
================================================
FILE: lib/trebuchet/version.rb
================================================
class Trebuchet
VERSION = "0.12.1".freeze
end
================================================
FILE: lib/trebuchet.rb
================================================
require 'digest/sha1'
require 'forwardable'
class Trebuchet
# initialize a single one to save object allocations
# Todo perhaps choose a better hash instead of sha1
SHA1 = Digest::SHA1.new
class << self
extend Forwardable
def_delegators :state, :current=, :current_block=, :current_block,
:logs, :admin_view, :admin_view=, :admin_edit, :admin_edit=,
:time_zone, :time_zone=, :author=, :author, :visitor_id
attr_accessor :exception_handler
attr_accessor :threadsafe_state
def backend
self.backend = :memory unless @backend
@backend
end
def set_backend(backend_type, *args)
if backend_type.is_a?(Symbol)
require "trebuchet/backend/#{backend_type}"
@backend = Backend.lookup(backend_type).new(*args)
elsif backend_type.class.name =~ /Trebuchet::Backend/
@backend = backend_type
end
end
# this only works with additional args, e.g.: Trebuchet.backend = :memory
alias_method :backend=, :set_backend
alias_method :threadsafe_state?, :threadsafe_state
# Logging done at class level
# TODO: split by user identifier so instance can return scoped to one user
# (in case multiple users have user.trebuchet called)
def initialize_logs
state.logs = {}
end
def log(feature_name, result)
initialize_logs if state.logs.nil?
logs[feature_name] = result
end
def logs
state.logs
end
def current=(other)
state.current = other
end
def current
state.current ||= current_block.call if current_block.respond_to?(:call)
state.current || new(nil) # return an blank Trebuchet instance if @current is not set
end
def reset_current!
self.current = nil
end
def thread_local_key
:trebuchet_state
end
# state is a representation of the current context of Trebuchet such as current and
# current_proc, which are expected to be different between threads or fibers.
# exception_handler and backend are not included in this state object as they are
# not expected to change from fiber to fiber or request to request, therefore they
# must be thread/fibersafe on their own accord.
def state
if threadsafe_state?
Thread.current[thread_local_key] ||= State.new
else
@state ||= State.new
end
end
def state=(new_state)
if threadsafe_state?
Thread.current[thread_local_key] = new_state
else
@state = new_state
end
end
def threadsafe_state?
threadsafe_state
end
end
def self.aim(feature_name, *args)
Feature.find(feature_name).aim(*args)
end
def self.dismantle(feature_name)
Feature.find(feature_name).dismantle
end
def self.dismantle_stubs
Feature.dismantle_stubs
end
def self.define_strategy(name, &block)
Strategy::Custom.define(name, block)
end
def self.define_request_aware_strategy(name, &block)
Strategy::CustomRequestAware.define(name, block)
end
def self.visitor_id=(id_or_proc)
if id_or_proc.is_a?(Proc)
state.visitor_id = id_or_proc
elsif id_or_proc.is_a?(Integer)
state.visitor_id = proc { |request| id_or_proc }
else
state.visitor_id = nil
end
end
def self.use_with_rails!
if defined?(ActionController::Base)
ActionController::Base.send(:include, Trebuchet::ActionController)
end
end
def self.feature(name)
Feature.find(name)
end
def initialize(current_user, request = nil)
@current_user = current_user
@request = request
@result_cache = {}
end
def launch(feature, &block)
if launch?(feature)
yield if block_given?
end
end
def launch?(feature)
result = @result_cache[feature]
if result.nil?
result = @result_cache[feature] =
!!Feature.find(feature).launch_at?(@current_user, @request)
Trebuchet.log(feature, result)
end
result
rescue => e
handle_exception(e, feature)
return false
end
def handle_exception(exception, feature = nil)
if self.class.exception_handler.is_a?(Proc)
argc = self.class.exception_handler.arity
argc = 3 if argc < 0
self.class.exception_handler.call *[exception, feature, self][0,argc]
end
end
def self.export
{}.tap do |features|
Trebuchet.backend.get_feature_names.map do |fn|
features[fn] = self.feature(fn).strategy.export
end
end
end
def self.history(include_archived = false)
return [] unless Trebuchet.backend.respond_to?(:get_all_history)
Trebuchet.backend.get_all_history(include_archived).map do |row|
[Time.at(row.first), Feature.find(row.last)]
end
end
end
require 'set'
require 'trebuchet/version'
require 'trebuchet/error'
require 'trebuchet/backend'
require 'trebuchet/backend/disabled'
# load other backends on demand so their dependencies can load first
require 'trebuchet/state'
require 'trebuchet/feature'
require 'trebuchet/strategy'
require 'trebuchet/strategy/base'
require 'trebuchet/strategy/custom'
require 'trebuchet/strategy/custom_request_aware'
require 'trebuchet/strategy/default'
require 'trebuchet/strategy/everyone'
require 'trebuchet/strategy/experiment'
require 'trebuchet/strategy/hostname'
require 'trebuchet/strategy/invalid'
require 'trebuchet/strategy/logic_and'
require 'trebuchet/strategy/logic_not'
require 'trebuchet/strategy/logic_or'
require 'trebuchet/strategy/multiple'
require 'trebuchet/strategy/nobody'
require 'trebuchet/strategy/percent'
require 'trebuchet/strategy/percent_deprecated'
require 'trebuchet/strategy/per_denomination'
require 'trebuchet/strategy/user_id'
require 'trebuchet/strategy/visitor_experiment'
require 'trebuchet/strategy/visitor_percent'
require 'trebuchet/strategy/visitor_percent_deprecated'
require 'trebuchet/strategy/stub'
require 'trebuchet/action_controller'
================================================
FILE: lib/trebuchet_rails/engine.rb
================================================
require 'rails'
require File.expand_path(File.dirname(__FILE__) + "/../trebuchet")
module TrebuchetRails
class Engine < Rails::Engine
isolate_namespace TrebuchetRails if respond_to?(:isolate_namespace)
end
end
Trebuchet.use_with_rails!
================================================
FILE: lib/trebuchet_rails.rb
================================================
require 'trebuchet_rails/engine'
require 'trebuchet'
================================================
FILE: spec/custom_request_aware_strategy_spec.rb
================================================
require 'spec_helper'
describe Trebuchet::Strategy::CustomRequestAware do
it "should launch according to the custom strategy with options" do
Trebuchet.define_request_aware_strategy(:ip_address_strategy) do |current_user, request, ip_address|
request[:ip_address] == ip_address
end
Trebuchet.aim('ip_limited_feature', :ip_address_strategy, '1.1.1.1')
Trebuchet.new(User.new, { :ip_address => '1.1.1.1' }).launch?('ip_limited_feature')
.should be_true
Trebuchet.new(User.new, { :ip_address => '2.2.2.2' }).launch?('ip_limited_feature')
.should be_false
end
it "should launch according to the custom strategy without options" do
Trebuchet.define_request_aware_strategy(:ip_address_strategy) do |current_user, request|
request[:ip_address] == '1.1.1.1'
end
Trebuchet.aim('ip_limited_feature', :ip_address_strategy)
Trebuchet.new(User.new, { :ip_address => '1.1.1.1' }).launch?('ip_limited_feature')
.should be_true
Trebuchet.new(User.new, { :ip_address => '2.2.2.2' }).launch?('ip_limited_feature')
.should be_false
end
it "should not explode when request is nil" do
Trebuchet.define_request_aware_strategy(:ip_address_strategy) do |current_user, request|
request[:ip_address] == '1.1.1.1'
end
Trebuchet.aim('ip_limited_feature', :ip_address_strategy)
Trebuchet.new(User.new).launch?('ip_limited_feature').should be_false
end
end
================================================
FILE: spec/custom_strategy_spec.rb
================================================
require 'spec_helper'
describe Trebuchet::Strategy::Custom do
it "should launch according to the custom strategy" do
Trebuchet.define_strategy(:admins) do |current_user|
current_user.has_role?(:admin)
end
Trebuchet.aim('admin_feature', :admins)
Trebuchet.new(User.new(1, :admin)).launch?('admin_feature').should be_true
Trebuchet.new(User.new(1, :user)).launch?('admin_feature').should be_false
end
it "should pass arguments to the custom strategy" do
Trebuchet.define_strategy(:role) do |current_user, role|
current_user.has_role?(role.to_sym)
end
Trebuchet.aim('power_feature', :role, :power_user)
Trebuchet.new(User.new(1, :power_user)).launch?('power_feature').should be_true
Trebuchet.new(User.new(1, :user)).launch?('power_feature').should be_false
end
it "should allow an always-on strategy" do
Trebuchet.define_strategy(:yes) { |user| true }
Trebuchet.aim("perma-feature", :yes)
Trebuchet.new(User.new 999).launch?("perma-feature").should be_true
Trebuchet.new(User.new nil).launch?("perma-feature").should be_true
end
it "should needs_user? based on block arity" do
# still a good idea to nilcheck within block however
pending "re-enable after switching to { |options, user, request| }"
Trebuchet.define_strategy(:yes) { |user| true }
Trebuchet.define_strategy(:heck_yeah) { |user, request| true }
Trebuchet.define_strategy(:never) { false }
Trebuchet.define_strategy(:always) { true }
Trebuchet::Strategy::Custom.new(:yes).needs_user?.should be_true
Trebuchet::Strategy::Custom.new(:heck_yeah).needs_user?.should be_true
Trebuchet::Strategy::Custom.new(:never).needs_user?.should be_false
Trebuchet::Strategy::Custom.new(:always).needs_user?.should be_false
end
end
================================================
FILE: spec/default_strategy_spec.rb
================================================
require 'spec_helper'
describe Trebuchet::Strategy::Default do
it "should not launch if no strategy was defined" do
Trebuchet.new(User.new(rand(2 << 32))).launch?('default').should be_false
end
it "should be named default" do
Trebuchet::Strategy::Default.strategy_name.should == :default
Trebuchet.feature('whatever').strategy.name.should == :default
end
it "should always return false" do
Trebuchet.feature('time_machine').aim(:default)
t = Trebuchet.new User.new(1)
t.launch?('time_machine').should === false
end
it "should return false when missing user" do
Trebuchet.feature('time_machine').aim(:default)
t = Trebuchet.new nil
t.launch?('time_machine').should === false
end
end
================================================
FILE: spec/disabled_backend_spec.rb
================================================
require 'spec_helper'
describe Trebuchet::Backend::Disabled do
before do
Trebuchet.backend = :disabled
Trebuchet.backend.should be_a(Trebuchet::Backend::Disabled)
end
it "should not store features" do
Trebuchet.feature('thing').aim(:users, [5]).aim(:percent, 9)
Trebuchet.feature('thing').strategy.should be_a(Trebuchet::Strategy::Default)
Trebuchet::Feature.all.should eql []
end
it "should always return false" do
Trebuchet.feature('time_machine').aim(:disabled)
t = Trebuchet.new User.new(1)
t.launch?('time_machine').should === false
end
after do
Trebuchet.backend = :memory
end
end
================================================
FILE: spec/everyone_strategy_spec.rb
================================================
require 'spec_helper'
describe Trebuchet::Strategy::Everyone do
it "should be named everyone" do
Trebuchet::Strategy::Everyone.strategy_name.should == :everyone
Trebuchet.feature('time_machine').aim(:everyone)
Trebuchet.feature('time_machine').strategy.name.should == :everyone
end
it "should always return true" do
Trebuchet.feature('time_machine').aim(:everyone)
t = Trebuchet.new User.new(1)
t.launch?('time_machine').should === true
end
it "should return true when missing user" do
Trebuchet.feature('time_machine').aim(:everyone)
t = Trebuchet.new nil
t.launch?('time_machine').should === true
end
end
================================================
FILE: spec/experiment_strategy_spec.rb
================================================
require 'spec_helper'
describe Trebuchet::Strategy::Experiment do
before do
@feature_name = "Photographic Memory"
@experiment_name = "Superhumanity"
end
it "should match a user in a bucket" do
Trebuchet.aim(@feature_name, :experiment, :name => @experiment_name, :bucket => 1)
strategy = Trebuchet.feature(@feature_name).strategy
# these values just happen to hash for the algorithm and experiment name
strategy.launch_at?(User.new(5)).should be_true
strategy.launch_at?(User.new(4)).should be_false
end
it "should adjust the number of buckets" do
Trebuchet.aim(@feature_name, :experiment, :name => @experiment_name, :bucket => 1, :total_buckets => 3)
strategy = Trebuchet.feature(@feature_name).strategy
strategy.total_buckets.should == 3
strategy.bucket.should == [1]
strategy.launch_at?(User.new(4)).should be_true
Trebuchet.aim(@feature_name, :experiment, :name => @experiment_name, :bucket => 2, :total_buckets => 3)
Trebuchet.feature(@feature_name).strategy.launch_at?(User.new(4)).should be_false
end
it "should be mutually exclusive within experiments" do
strategies = (1..10).map do |i|
Trebuchet.aim(@feature_name, :experiment, :name => @experiment_name, :bucket => i)
Trebuchet.feature(@feature_name).strategy
end
user_ids = (1..100).to_a
launches = strategies.map do |strategy|
user_ids.select {|user_id| strategy.launch_at?(User.new(user_id))}
end
occurrences = user_ids.map do |user_id|
launches.select{|l| l.include?(user_id)}.size
end
# no user should be in more than one bucket
occurrences.select{|i| i > 1}.size.should == 0
# each user should be in one bucket
occurrences.select{|i| i < 1}.size.should == 0
end
it "should distribute users evenly" do
Trebuchet.aim(@feature_name, :experiment, :name => @experiment_name, :bucket => 1)
strategy = Trebuchet.feature(@feature_name).strategy
user_ids = (1..10_000).to_a
launches = user_ids.map {|user_id| strategy.launch_at?(User.new(user_id))}
# total should be around 20% (spread evenly across default of 5 buckets)
launch_count = launches.select{|l| l == true}.size
(launch_count * 100 / user_ids.size).round.should == 20
end
it "should have low overlap between experiments" do
other_experiment_name = 'World Peace'
other_feature_name = 'Friendship Rings'
another_experiment_name = 'Space Tourism'
another_feature_name = 'Orbital Disneyland'
Trebuchet.aim(@feature_name, :experiment, :name => @experiment_name, :bucket => 1)
Trebuchet.aim(other_feature_name, :experiment, :name => other_experiment_name, :bucket => 1)
Trebuchet.aim(another_feature_name, :experiment, :name => another_experiment_name, :bucket => 1)
strategies = [
Trebuchet.feature(@feature_name).strategy,
Trebuchet.feature(other_feature_name).strategy,
Trebuchet.feature(another_feature_name).strategy
]
# find out which users match each strategy
user_ids = (1..10_000).to_a
launches = strategies.map do |strategy|
user_ids.select {|user_id| strategy.launch_at?(User.new(user_id))}
end
# intersect each set with the next
overlaps = []
(0..launches.size).each do |i|
j = (i + 1) % launches.size
overlaps << launches[i] & launches[j]
end
# each group should have about 20% overlap (for 5 buckets)
((19..21) === (overlaps[0].size * 100 / user_ids.size).round).should be_true
((19..21) === (overlaps[1].size * 100 / user_ids.size).round).should be_true
((19..21) === (overlaps[2].size * 100 / user_ids.size).round).should be_true
# 1% or fewer of users should be in all three groups
total_overlap = (launches[0] & launches[1] & launches[2])
(total_overlap.size * 100 / user_ids.size).round.should < 2
end
it "should return false for invalid parameters" do
Trebuchet.aim(@feature_name, :experiment, :name => @experiment_name, :bucket => 900)
Trebuchet.feature(@feature_name).strategy.should_not be_valid
Trebuchet.aim(@feature_name, :experiment, :name => @experiment_name, :bucket => 1, :total_buckets => -17)
Trebuchet.feature(@feature_name).strategy.should_not be_valid
Trebuchet.aim(@feature_name, :experiment, :name => @experiment_name, :bucket => 10, :total_buckets => 3)
Trebuchet.feature(@feature_name).strategy.should_not be_valid
Trebuchet.aim(@feature_name, :experiment, :name => @experiment_name)
Trebuchet.feature(@feature_name).strategy.should_not be_valid
Trebuchet.aim(@feature_name, :experiment, :bucket => 1)
Trebuchet.feature(@feature_name).strategy.should_not be_valid
end
end
================================================
FILE: spec/feature_spec.rb
================================================
require 'spec_helper'
describe Trebuchet::Feature do
def feature
Trebuchet.feature('some_feature')
end
def feature_names
Trebuchet.backend.get_feature_names
end
def archived_feature_names
Trebuchet.backend.get_archived_feature_names
end
describe :aim do
it "should add one strategy" do
feature.aim(:percent, 10)
feature.strategy.name.should be :percent
end
it "should add multiple strategies by chaining" do
feature.aim(:default)
feature.strategy.name.should be :default
feature.aim(:percent, 10).aim(:users, 1)
feature.strategy.name.should be :multiple
strategy_names = feature.strategy.strategies.map{ |s| s.name }
strategy_names.should include(:percent)
strategy_names.should include(:users)
end
it "should obliterate chained strategies" do
feature.aim(:percent, 10).aim(:users, 1)
feature.aim(:default)
feature.strategy.name.should be :default
end
end
describe :adjust do
it "should add one strategy" do
feature.dismantle
feature.adjust(:percent, 10)
feature.strategy.name.should be :percent
end
it "should adjust a strategy in the chain" do
feature.aim(:percent, 10).aim(:users, 1)
feature.strategy.name.should be :multiple
feature.adjust(:users, 2)
feature.strategy.name.should be :multiple
user_strategy = feature.strategy.strategies.detect { |s| s.name == :users }
user_strategy.user_ids.should include(2)
user_strategy.user_ids.should_not include(1)
feature.adjust(:percent, 20)
percent_strategy = feature.strategy.strategies.detect { |s| s.name == :percent }
percent_strategy.percentage.should == 20
end
it "should not obliterate chained strategies" do
feature.aim(:percent, 10).aim(:users, 1)
feature.adjust(:default)
feature.strategy.name.should be :multiple
feature.strategy.strategies.map{ |s| s.name }.should include(:default)
end
end
describe :augment do
it "should add one strategy where none exist" do
feature.dismantle
feature.augment(:percent, 10)
feature.strategy.name.should be :percent
end
it "should append a strategy if others exist" do
feature.aim(:percent, 10)
feature.augment(:users, 1)
feature.strategy.name.should be :multiple
percent_strategy = feature.strategy.strategies.detect { |s| s.name == :percent }
percent_strategy.percentage.should == 10
user_strategy = feature.strategy.strategies.detect { |s| s.name == :users }
user_strategy.user_ids.should include(1)
end
it "should adjust an existing strategy with numeric" do
feature.aim(:percent, 10)
feature.augment(:percent, 5)
feature.strategy.name.should be :percent
feature.strategy.percentage.should be 15
feature.augment(:percent, 5.0)
feature.strategy.percentage.should be 20
end
it "should adjust an existing strategy with set/array" do
feature.aim(:users, [1])
feature.augment(:users, [2,3])
feature.strategy.name.should == :users
feature.strategy.user_ids.sort.should == [1,2,3]
feature.augment(:users, [4])
feature.strategy.user_ids.sort.should == [1,2,3,4]
# this probably should not be allowed
# feature.aim(:users, 1)
# feature.augment(:users, 2)
# feature.strategy.user_ids.sort.should == [1,2]
end
it "should adjust an existing strategy with hash" do
Trebuchet.define_strategy(:role_percent) do |user, options|
percent = options[user.role].to_i
user.id % 100 < percent
end
old_percentages = {:admin => 30, :editor => 50, :publisher => 100}
feature.aim(:role_percent, old_percentages)
new_percentages = {:admin => 100, :reviewer => 10}
feature.augment(:role_percent, new_percentages)
feature.strategy.name.should be :custom
feature.strategy.custom_name.should be :role_percent
feature.strategy.options.should == (old_percentages.merge(new_percentages))
end
end
describe :dismantle do
it "should remove a feature and add it to archived features" do
feature.aim(:users, [1])
feature_names.include?(feature.name).should be_true
feature.dismantle
feature_names.include?(feature.name).should be_false
archived_feature_names.include?(feature.name).should be_true
end
it "should remove archived feature when dismantled feature is redefined" do
feature.dismantle
feature_names.include?(feature.name).should be_false
archived_feature_names.include?(feature.name).should be_true
feature.aim(:percent, 5)
feature_names.include?(feature.name).should be_true
archived_feature_names.include?(feature.name).should be_false
end
end
end
================================================
FILE: spec/logic_and_strategy_spec.rb
================================================
require 'spec_helper'
describe Trebuchet::Strategy::LogicAnd do
it "passes the check only when all conditions are met - case 1" do
Trebuchet.feature('pokemon').aim(
:logic_and,
{
percent: 0,
users: [30, 35],
},
)
t = Trebuchet.new(User.new(30))
t.launch?('pokemon').should === false
t = Trebuchet.new(User.new(111))
t.launch?('pokemon').should === false
end
it "passes the check only when all conditions are met - case 2" do
Trebuchet.feature('pokemon').aim(
:logic_and,
{
percent: 100,
users: [30, 35],
},
)
t = Trebuchet.new(User.new(30))
t.launch?('pokemon').should === true
t = Trebuchet.new(User.new(111))
t.launch?('pokemon').should === false
end
it "nests well" do
Trebuchet.feature('pokemon').aim(
:logic_and,
{
"percent" => 100,
"logic_and" => {
"users" => [90, 91, 92],
"logic_not" => {
"users" => [90],
},
},
},
)
[91, 92].each do |uid|
t = Trebuchet.new(User.new(uid))
t.launch?('pokemon').should === true
end
[1, 3, 5, 7, 90].each do |uid|
t = Trebuchet.new(User.new(uid))
t.launch?('pokemon').should === false
end
end
end
================================================
FILE: spec/logic_base_strategy_spec.rb
================================================
require 'spec_helper'
describe Trebuchet::Strategy::LogicBase do
it "should set @feature on sub-strategies" do
feature = Trebuchet.feature('pokemon')
feature.aim(
:logic_and,
{
percent: 100,
users: [30, 35],
},
)
feature.strategy.feature.name.should == 'pokemon'
feature.strategy.strategies.first.feature.name.should == 'pokemon'
feature.strategy.strategies.last.feature.name.should == 'pokemon'
end
it "should pass user and request to each strategy" do
args = [:foo, 1]
user = mock "User"
request = mock "Request"
strategy = mock "Strategy"
Trebuchet::Strategy.should_receive(:find).with(*args).and_return(strategy)
strategy.should_receive(:launch_at?).with(user, request)
strategy.should_receive(:needs_user?).and_return(false)
s = Trebuchet::Strategy::LogicOr.new({foo: 1})
s.launch_at?(user, request)
end
end
================================================
FILE: spec/logic_not_strategy_spec.rb
================================================
require 'spec_helper'
describe Trebuchet::Strategy::LogicNot do
it "works expectedly as a not operator" do
Trebuchet.feature('pokemon').aim(
:logic_not,
{
users: [30, 35],
},
)
t = Trebuchet.new(User.new(30))
t.launch?('pokemon').should === false
t = Trebuchet.new(User.new(111))
t.launch?('pokemon').should === true
end
end
================================================
FILE: spec/logic_or_strategy_spec.rb
================================================
require 'spec_helper'
describe Trebuchet::Strategy::LogicOr do
it "passes the check as long as one of the children strategies passes" do
Trebuchet.feature('pokemon').aim(
:logic_or,
{
users: [30, 35],
everyone: nil,
nobody: nil,
percent: 30,
},
)
[1, 9, 91, 92, 2016].each do |uid|
t = Trebuchet.new(User.new(uid))
t.launch?('pokemon').should === true
end
end
it "is not launching if no children strategy works" do
Trebuchet.feature('pokemon').aim(
:logic_or,
{
percent: 0,
users: [30, 35],
},
)
t = Trebuchet.new(User.new(30))
t.launch?('pokemon').should === true
t = Trebuchet.new(User.new(111))
t.launch?('pokemon').should === false
end
it "nests well" do
Trebuchet.feature('pokemon').aim(
:logic_or,
{
users: [100],
logic_and: {
users: [90, 91, 92],
logic_not: {
users: [90],
},
},
},
)
[91, 92, 100].each do |uid|
t = Trebuchet.new(User.new(uid))
t.launch?('pokemon').should === true
end
[1, 3, 5, 7, 90].each do |uid|
t = Trebuchet.new(User.new(uid))
t.launch?('pokemon').should === false
end
end
end
================================================
FILE: spec/multiple_strategy_spec.rb
================================================
require 'spec_helper'
describe Trebuchet::Strategy::Multiple do
it "should support chaining strategies" do
Trebuchet.feature('time_machine').aim(:percent, 5).aim(:users, [10, 11])
should_launch('time_machine', [31, 36, 10, 11, 197])
should_not_launch('time_machine', [49, 71])
end
it "should always return booleans" do
Trebuchet.feature('time_machine').aim(:percent, 0).aim(:users, [5])
t = Trebuchet.new User.new(5)
t.launch?('time_machine').should === true
t = Trebuchet.new User.new(117)
t.launch?('time_machine').should === false
end
it "should set @feature on sub-strategies" do
feature = Trebuchet.feature('time_machine')
feature.aim(:percent, 10).aim(:users, [5])
feature.strategy.feature.name == feature.name
feature.strategy.strategies.first.feature.name.should == feature.name
feature.strategy.strategies.last.feature.name.should == feature.name
end
it "should pass user and request to each strategy" do
args = [:foo, 1]
user = mock "User"
request = mock "Request"
strategy = mock "Strategy"
Trebuchet::Strategy.should_receive(:find).with(*args).and_return(strategy)
strategy.should_receive(:launch_at?).with(user, request)
multi = Trebuchet::Strategy::Multiple.new(args)
multi.launch_at?(user, request)
end
it "should always return false for needs_user?" do
s = Trebuchet::Strategy::Multiple.new [:default, nil, :invalid, nil]
s.needs_user?.should be_false
s = Trebuchet::Strategy::Multiple.new [:default, nil, :percent, 5]
s.needs_user?.should be_false
end
it "should skip needs_user? sub-strategies if user not present" do
s = Trebuchet::Strategy::Multiple.new [:hostname, 'abc', :users, [1,2,3]]
s.strategies.first.should_receive(:launch_at?)
s.strategies.last.should_not_receive(:launch_at?)
s.launch_at?(nil)
end
end
================================================
FILE: spec/nobody_strategy_spec.rb
================================================
require 'spec_helper'
describe Trebuchet::Strategy::Nobody do
it "should be named nobody" do
Trebuchet::Strategy::Nobody.strategy_name.should == :nobody
Trebuchet.feature('time_machine').aim(:nobody)
Trebuchet.feature('time_machine').strategy.name.should == :nobody
end
it "should always return false" do
Trebuchet.feature('time_machine').aim(:nobody)
t = Trebuchet.new User.new(1)
t.launch?('time_machine').should === false
end
it "should return false when missing user" do
Trebuchet.feature('time_machine').aim(:nobody)
t = Trebuchet.new nil
t.launch?('time_machine').should === false
end
end
================================================
FILE: spec/per_denomination_strategy_spec.rb
================================================
require 'spec_helper'
describe Trebuchet::Strategy::PerDenomination do
describe 'launch' do
before(:each) do
Trebuchet.aim('strategy', :per_denomination, { numerator: 50, denominator: 1000 })
end
it "should not launch to unsaved users" do
Trebuchet.new(nil).launch?('strategy').should be_false
end
it "should not launch to users with no IDs" do
Trebuchet.new(User.new(nil)).launch?('strategy').should be_false
end
it "should launch to the correct per_denomination of users" do
launched_users =
(1..10_000).select { |i| Trebuchet.new(User.new(i)).launch?('strategy') }.size
unlaunched_users =
(1..10_000).reject { |i| Trebuchet.new(User.new(i)).launch?('strategy') }.size
# If you run this to larger N it approaches .95 and 0.05. This is
# hash function dependent but it's a nice santity check.
launched_users.should be_within(200).of(500)
unlaunched_users.should be_within(200).of(9500)
end
end
it "should not yank the feature from users when per_denomination is increased" do
Trebuchet.aim('strategy', :per_denomination, { numerator: 10, denominator: 1000 })
first_launched_users =
(1..10_000).select { |i| Trebuchet.new(User.new(i)).launch?('strategy') }
Trebuchet.aim('strategy', :per_denomination, { numerator: 50, denominator: 1000 })
second_launched_users =
(1..10_000).select { |i| Trebuchet.new(User.new(i)).launch?('strategy') }
(second_launched_users & first_launched_users).should eq first_launched_users
end
it "should distribute launches based on the feature name" do
Trebuchet.aim('strategy1', :per_denomination, { numerator: 50, denominator: 1000 })
Trebuchet.aim('strategy2', :per_denomination, { numerator: 50, denominator: 1000 })
per_denomination1_launched_users =
(1..10_000).select { |i| Trebuchet.new(User.new(i)).launch?('strategy1') }
per_denomination2_launched_users =
(1..10_000).select { |i| Trebuchet.new(User.new(i)).launch?('strategy2') }
per_denomination1_launched_users.should_not eq per_denomination2_launched_users
end
it "should always return booleans" do
Trebuchet.aim('strategy', :per_denomination, { numerator: 50, denominator: 1000 })
(1..10_000).each do |i|
[true, false].should include(Trebuchet.new(User.new(i)).launch?('strategy'))
end
end
it "should handle 0 numerator" do
Trebuchet.aim('strategy', :per_denomination, { numerator: 0, denominator: 1000 })
should_not_launch 'strategy', (1..10_000).to_a
end
it "should fail on 0 denominator" do
Trebuchet.aim('strategy', :per_denomination, { numerator: 1000, denominator: 0 })
should_not_launch 'strategy', (1..10_000).to_a # trebuchet silently fails all invalid strategies
end
it "should handle numerator == denominator" do
Trebuchet.aim('strategy', :per_denomination, { numerator: 1000, denominator: 1000 })
should_launch 'strategy', (1..10_000).to_a
end
it "should fail when numerator > denominator" do
Trebuchet.aim('strategy', :per_denomination, { numerator: 1001, denominator: 1000 })
should_not_launch 'strategy', (1..10_000).to_a # trebuchet silently fails all invalid strategies
end
it "should handle garbage arguments" do
ids = (1..20_000).to_a
Trebuchet.feature('strategy').aim(:per_denomination, -1)
should_not_launch 'strategy', ids
Trebuchet.feature('strategy').aim(:per_denomination, -150)
should_not_launch 'strategy', ids
Trebuchet.feature('strategy').aim(:per_denomination, 'h')
should_not_launch 'strategy', ids
Trebuchet.feature('strategy').aim(:per_denomination, 'h')
should_not_launch 'strategy', ids
Trebuchet.feature('strategy').aim(:per_denomination, [5, 10])
should_not_launch 'strategy', ids
Trebuchet.feature('strategy').aim(:per_denomination, 20..50)
should_not_launch 'strategy', ids
Trebuchet.feature('strategy').aim(:per_denomination, nil)
should_not_launch 'strategy', ids
Trebuchet.feature('strategy').aim(:per_denomination, :from => 7)
should_not_launch 'strategy', ids
Trebuchet.feature('strategy').aim(:per_denomination, :to => 1)
should_not_launch 'strategy', ids
end
end
================================================
FILE: spec/percent_deprecated_strategy_spec.rb
================================================
require 'spec_helper'
describe Trebuchet::Strategy::PercentDeprecated do
it "should be deprecated" do
Trebuchet::Feature.with_deprecated_strategies_enabled(false) do
expect {
Trebuchet.aim('percentage', :percent_deprecated, 5)
}.to raise_error(/deprecated/)
end
end
it "should not launch to unsaved users, users with no IDs" do
Trebuchet.aim('percentage', :percent_deprecated, 5)
Trebuchet.new(nil).launch?('percentage').should be_false
Trebuchet.new(User.new(nil)).launch?('percentage').should be_false
end
def should_only_launch_to_a_percentage_of_users(feature_name)
Trebuchet.aim('percentage', feature_name, 5)
offset = Trebuchet.feature('percentage').strategy.offset
should_launch 'percentage', offset_ids([0, 1, 2, 3, 4, 100, 101, 102, 103, 104], offset)
should_not_launch('percentage', [5, 6, 105, 106].map{|i| i - offset})
end
it "should only launch to a percentage of users" do
should_only_launch_to_a_percentage_of_users(:percent_deprecated)
end
it "should not yank the feature from users when percentage is increased" do
Trebuchet.aim('percentage', :percent_deprecated, 2)
offset = Trebuchet.feature('percentage').strategy.offset
should_launch 'percentage', offset_ids([0, 1], offset)
should_not_launch 'percentage', offset_ids([2, 3], offset)
Trebuchet.aim('percentage', :percent_deprecated, 4)
should_launch 'percentage', offset_ids([0, 1, 2, 3], offset)
end
it "should create an offset based on the feature name" do
Trebuchet.aim('percentage', :percent_deprecated, 1)
offset = Trebuchet.feature('percentage').strategy.offset
user_id = offset_ids([0], offset).first
should_launch 'percentage', user_id
should_not_launch 'percentage', [user_id - 1]
should_not_launch 'percentage', [user_id + 1]
end
it "should always return booleans" do
Trebuchet.feature('percentage').aim(:percent_deprecated, 1)
offset = Trebuchet.feature('percentage').strategy.offset
should_launch 'percentage', offset_ids([0], offset)
t = Trebuchet.new User.new offset_ids([0], offset).first
t.launch?('percentage').should === true
t = Trebuchet.new User.new(0 - (offset - 1))
t.launch?('percentage').should === false
end
def offset_ids(ids, offset)
ids.to_a.map {|id| id + offset % 100}
end
it "should launch to the right users with percentage" do
Trebuchet.feature('freedom').aim(:percent_deprecated, :percentage => 50)
offset = Trebuchet.feature('freedom').strategy.offset
should_launch 'freedom', offset_ids(0..49, offset)
should_not_launch 'freedom', offset_ids(50..99, offset)
should_launch 'freedom', offset_ids(100..149, offset)
should_not_launch 'freedom', offset_ids(150..199, offset)
should_launch'freedom', offset_ids([200, 201], offset)
should_not_launch 'freedom', offset_ids([250, 251], offset)
end
it "should launch to the right users with wrap" do
Trebuchet.feature('freedom').aim(:percent_deprecated, :from => 99, :to => 1)
should_launch 'freedom', [0, 1, 99, 100, 101, 199, 200, 201, 299]
should_not_launch 'freedom', [97, 98, 102, 103, 198, 202, 298, 302]
end
it "should launch to 1%" do
Trebuchet.feature('freedom').aim(:percent_deprecated, :percentage => 1)
offset = Trebuchet.feature('freedom').strategy.offset
should_launch 'freedom', offset_ids([100, 200, 300, 0], offset)
should_not_launch 'freedom', offset_ids([1, 99, 101, 198, 202, 350], offset)
end
it "should handle 0%" do
Trebuchet.feature('freedom').aim(:percent_deprecated, 0)
should_not_launch 'freedom', (1..200).to_a
end
it "should handle garbage arguments" do
ids = (1..200).to_a
Trebuchet.feature('freedom').aim(:percent_deprecated, -1)
should_not_launch 'freedom', ids
Trebuchet.feature('freedom').aim(:percent_deprecated, -150)
should_not_launch 'freedom', ids
Trebuchet.feature('freedom').aim(:percent_deprecated, 'h')
should_not_launch 'freedom', ids
Trebuchet.feature('freedom').aim(:percent_deprecated, 'h')
should_not_launch 'freedom', ids
Trebuchet.feature('freedom').aim(:percent_deprecated, [5, 10])
should_not_launch 'freedom', ids
Trebuchet.feature('freedom').aim(:percent_deprecated, 20..50)
should_not_launch 'freedom', ids
Trebuchet.feature('freedom').aim(:percent_deprecated, nil)
should_not_launch 'freedom', ids
Trebuchet.feature('freedom').aim(:percent_deprecated, :from => 7)
should_not_launch 'freedom', ids
Trebuchet.feature('freedom').aim(:percent_deprecated, :to => 1)
should_not_launch 'freedom', ids
end
it "works with from/to > 100" do
# just documenting this side-effect
Trebuchet.feature('freedom').aim(:percent_deprecated, :from => 105, :to => 210)
should_launch 'freedom', [5, 106, 207, 308, 409, 1210]
should_not_launch 'freedom', [4, 99, 111, 150, 215]
end
end
================================================
FILE: spec/percent_strategy_spec.rb
================================================
require 'spec_helper'
describe Trebuchet::Strategy::Percent do
it "should not launch to unsaved users, users with no IDs" do
Trebuchet.aim('percentage', :percent, 5)
Trebuchet.new(nil).launch?('percentage').should be_false
Trebuchet.new(User.new(nil)).launch?('percentage').should be_false
end
it "should only launch to a percentage of users" do
Trebuchet.aim('percentage', :percent, 5)
should_launch 'percentage', [7, 24, 74, 75]
should_not_launch('percentage', [47, 48, 49, 76, 78])
end
it "should launch to the correct percentage of users" do
Trebuchet.aim('percentage', :percent, 5)
map = Hash.new {|a,k| a[k] = 0 }
n = 20000
n.times do |i|
map[Trebuchet.new(User.new(i)).launch?('percentage')] += 1
end
map.each {|k, v|
map[k] = v / n.to_f
}
# If you run this to larger N it approaches .95 and 0.05. This is
# hash function dependent but it's a nice santity check.
map[false].should be_within(0.02).of(0.95)
map[true].should be_within(0.02).of(0.05)
end
it "should not yank the feature from users when percentage is increased" do
Trebuchet.aim('percentage', :percent, 2)
should_launch 'percentage', [7, 74]
should_not_launch 'percentage', [24, 147]
Trebuchet.aim('percentage', :percent, 4)
should_launch 'percentage', [7, 74, 24, 147]
end
it "should distribute launches based on the feature name" do
Trebuchet.aim('percentage1', :percent, 1)
Trebuchet.aim('percentage2', :percent, 1)
should_launch 'percentage1', [9, 145, 186]
should_not_launch 'percentage2', [9, 145, 186]
should_launch 'percentage2', [21, 47]
should_not_launch 'percentage1', [21, 47]
end
it "should always return booleans" do
Trebuchet.aim('percentage1', :percent, 1)
t = Trebuchet.new(User.new(9))
t.launch?('percentage1').should === true
t = Trebuchet.new(User.new(21))
t.launch?('percentage1').should === false
end
it "should handle 0%" do
Trebuchet.feature('freedom').aim(:percent, 0)
should_not_launch 'freedom', (1..200).to_a
end
it "should handle garbage arguments" do
ids = (1..200).to_a
Trebuchet.feature('freedom').aim(:percent, -1)
should_not_launch 'freedom', ids
Trebuchet.feature('freedom').aim(:percent, -150)
should_not_launch 'freedom', ids
Trebuchet.feature('freedom').aim(:percent, 'h')
should_not_launch 'freedom', ids
Trebuchet.feature('freedom').aim(:percent, 'h')
should_not_launch 'freedom', ids
Trebuchet.feature('freedom').aim(:percent, [5, 10])
should_not_launch 'freedom', ids
Trebuchet.feature('freedom').aim(:percent, 20..50)
should_not_launch 'freedom', ids
Trebuchet.feature('freedom').aim(:percent, nil)
should_not_launch 'freedom', ids
Trebuchet.feature('freedom').aim(:percent, :from => 7)
should_not_launch 'freedom', ids
Trebuchet.feature('freedom').aim(:percent, :to => 1)
should_not_launch 'freedom', ids
end
end
================================================
FILE: spec/redis_backend_spec.rb
================================================
require 'spec_helper'
require 'mock_redis'
require 'trebuchet/backend/redis'
describe Trebuchet::Backend::Redis do
before(:all) do
@backend = Trebuchet.backend
end
before(:each) do
Trebuchet.set_backend :disabled
end
it "should set backend to redis with defaults" do
Trebuchet.backend = :redis
Trebuchet.backend.should be_a(Trebuchet::Backend::Redis)
Trebuchet.set_backend :redis
Trebuchet.backend.should be_a(Trebuchet::Backend::Redis)
end
it "should set redis client if passed in" do
r = Redis.new
Redis.stub!(:new).and_return(nil)
lambda {Trebuchet.set_backend :redis}.should raise_error Trebuchet::BackendInitializationError
Trebuchet.backend.should be_a(Trebuchet::Backend::Disabled)
Trebuchet.backend.instance_variable_get(:@redis).should be_nil
Trebuchet.set_backend :redis, :client => r
Trebuchet.backend.instance_variable_get(:@redis).should eql r
end
it "should pass arguments to Redis.new" do
r = Redis.new
Redis.should_receive(:new).with(:this => 'that', :other => true).and_return(r)
Trebuchet.set_backend :redis, :this => 'that', :other => true
args = [:seven, {8 => 'nine'}]
Redis.should_receive(:new).with(*args).and_return(r)
Trebuchet.set_backend :redis, *args
end
after(:all) do
# cleanup
Trebuchet.backend = @backend
end
end
================================================
FILE: spec/redis_hammerspaced_spec.rb
================================================
require 'spec_helper'
require 'mock_redis'
require 'trebuchet/backend/redis_hammerspaced'
describe Trebuchet::Backend::RedisHammerspaced do
before(:all) do
@backend = Trebuchet.backend
end
before(:each) do
Trebuchet.set_backend :disabled
end
it "should properly get empty feature names and strategies" do
r = Redis.new
Redis.stub!(:new).and_return(nil)
Trebuchet.backend.should be_a(Trebuchet::Backend::Disabled)
Trebuchet.backend.instance_variable_get(:@redis).should be_nil
Trebuchet.set_backend :redis_hammerspaced,
:client => r,
:hammerspace => {},
:skip_check => true
Trebuchet.backend.get_feature_names.should eq []
Trebuchet.backend.get_strategy("adfd").should be_nil
end
it "should properly get feature name list and non-empty strategies" do
r = Redis.new
Redis.stub!(:new).and_return(nil)
Trebuchet.backend.should be_a(Trebuchet::Backend::Disabled)
Trebuchet.backend.instance_variable_get(:@redis).should be_nil
Trebuchet.set_backend :redis_hammerspaced,
:client => r,
:hammerspace => {
"trebuchet/feature-names" => ["foo", "bar"].to_s, # stringified array
"trebuchet/features/foo" => {
"everyone" => [nil],
"users" => [[1, 2, 3]],
}.to_json, # stringified json
},
:skip_check => true
Trebuchet.backend.get_feature_names.should eq ["foo", "bar"]
Trebuchet.backend.get_strategy("foo").should eq [:everyone, nil, :users, [1, 2, 3]]
end
it "should properly invalidate local cache" do
r = Redis.new
Redis.stub!(:new).and_return(nil)
hammerspace = {
"trebuchet/feature-names" => ["foo", "bar"].to_s,
"trebuchet/features/foo" => {
"everyone" => [nil],
"users" => [[1, 2, 3]],
}.to_json
}
def hammerspace.uid
@uid ||= Time.now().to_i
@uid = @uid + 1
end
def hammerspace.close
end
Trebuchet.set_backend :redis_hammerspaced,
:client => r,
:hammerspace => hammerspace,
:skip_check => true
# Force to load strategy to local cache
Trebuchet.backend.get_strategy("foo")
hammerspace["trebuchet/features/foo"] = {
"everyone" => [nil],
"users" => [[1, 2]],
}.to_json
Trebuchet.backend.get_strategy("foo").should eq [:everyone, nil, :users, [1, 2, 3]]
# after refresh we should have the up-to-date strategy
Trebuchet.backend.refresh
Trebuchet.backend.get_strategy("foo").should eq [:everyone, nil, :users, [1, 2]]
end
it "should properly generate hammerspace hash" do
r = Redis.new
Redis.stub!(:new).and_return(nil)
Trebuchet.set_backend :redis_hammerspaced,
:client => r,
:hammerspace => {},
:skip_check => true
feature_names = ["foo", "bar"]
features = [
["everyone", "[null]", "user", "[[1,2,3]]"], # foo
["visitor_experiment", "[{\"name\":\"rate\",\"total_buckets\":5,\"bucket\":1}]"] # bar
]
foo_string = {
"everyone" => [nil],
"user" => [[1,2,3]],
}.to_json
bar_string = {
"visitor_experiment" => [
{
"name" => "rate",
"total_buckets" => 5,
"bucket" => 1,
}
],
}.to_json
last_updated = Time.now.to_i
expected_hash = {
"trebuchet/feature-names" => feature_names.to_json,
"trebuchet/features/foo" => foo_string,
"trebuchet/features/bar" => bar_string,
"trebuchet/last_updated" => last_updated.to_s,
}
Trebuchet.backend.generate_hammerspace_hash(
feature_names,
features,
last_updated
).should eq expected_hash
end
after(:all) do
# cleanup
Trebuchet.backend = @backend
end
end
================================================
FILE: spec/spec_helper.rb
================================================
$:.unshift File.dirname(__FILE__) + '/../lib'
require 'bundler'
# Bundler breaks things
Bundler.require :default, :test
require 'mock_redis'
class Redis < MockRedis ; end
require 'trebuchet'
require 'user'
RSpec.configure do |config|
config.around(:each) { |ex|
Trebuchet::Feature.with_deprecated_strategies_enabled(&ex)
}
end
# # uncomment to run specs against Redis backend instead of Memory backend
# require 'redis'
# Trebuchet.set_backend :redis, Redis.new(:host => '127.0.0.1', :port => 6379)
def should_launch(feature, users)
should_or_should_not_launch(feature, users, be_true)
end
def should_not_launch(feature, users)
should_or_should_not_launch(feature, users, be_false)
end
def should_or_should_not_launch(feature, users, be_true_or_false)
Array(users).each do |user_or_user_id|
user = user_or_user_id.is_a?(User) ? user_or_user_id : User.new(user_or_user_id)
Trebuchet.new(user).launch?(feature).should be_true_or_false
end
end
def mock_request(cookie = nil)
mock 'Request', :cookies => {:visitor => cookie}
end
================================================
FILE: spec/stubbing_spec.rb
================================================
require 'spec_helper'
describe Trebuchet::Feature::Stubbing do
before do
Trebuchet.dismantle_stubs
end
describe '#stub' do
it "should stub a feature as launched" do
Trebuchet.feature('test').stub(:launched)
should_launch('test', [0])
end
it "should stub a feature as not launched" do
Trebuchet.feature('test').stub(:not_launched)
should_not_launch('test', [0])
end
it "should employ the stub strategy for features stubbed launched" do
Trebuchet.feature('test').stub(:launched)
Trebuchet.feature('test').strategy.is_a?(Trebuchet::Strategy::Stub)
end
it "should employ the stub strategy for features stubbed not launched" do
Trebuchet.feature('test').stub(:not_launched)
Trebuchet.feature('test').strategy.is_a?(Trebuchet::Strategy::Stub)
end
end
describe '#stubbed?' do
it "should report when features are stubbed launched" do
Trebuchet.feature('test').stub(:launched)
Trebuchet.feature('test').should be_stubbed
end
it "should report when features are stubbed not launched" do
Trebuchet.feature('test').stub(:not_launched)
Trebuchet.feature('test').should be_stubbed
end
end
describe '#dismantle_stubs' do
it "should reset stubbed features" do
Trebuchet.feature('test').stub(:launched)
Trebuchet.dismantle_stubs
Trebuchet::Feature.stubbed_features.should be_empty
end
it "should restore them to their default state" do
Trebuchet.feature('test').stub(:launched)
Trebuchet.dismantle_stubs
should_not_launch('test', [0])
end
end
describe '#stubbed_features' do
it "should report when feature are stubbed" do
Trebuchet.feature('test').stub(:launched)
Trebuchet::Feature.stubbed_features.count.should == 1
end
it "should report when features are stubbed launched" do
Trebuchet.feature('test').stub(:launched)
Trebuchet::Feature.stubbed_features['test'].should == :launched
end
it "should report when features are stubbed not launched" do
Trebuchet.feature('test').stub(:not_launched)
Trebuchet::Feature.stubbed_features['test'].should == :not_launched
end
end
end
================================================
FILE: spec/trebuchet_spec.rb
================================================
require 'spec_helper'
describe Trebuchet do
describe "launch?" do
it "should call launch_at? on feature" do
Trebuchet::Feature.any_instance.should_receive(:launch_at?).once
Trebuchet.new(User.new(1)).launch?('highly_experimental')
end
it "should call launch_at? on feature even if missing user" do
Trebuchet::Feature.any_instance.should_receive(:launch_at?).once
Trebuchet.new(nil).launch?('highly_experimental')
end
it "caches value of launch_at?" do
t = Trebuchet.new(nil)
Trebuchet.feature('highly_experimental').should_receive(:launch_at?).once
Trebuchet.feature('waste_of_time').should_receive(:launch_at?).once
t.launch?('highly_experimental')
t.launch?('waste_of_time')
t.launch?('highly_experimental')
t.launch?('waste_of_time')
end
end
describe "launch" do
it "should execute a block" do
times = 0
Trebuchet.aim('highly_experimental', :users, [1,2])
(Trebuchet.new(User.new(1)).launch('highly_experimental') { times += 1 }).should be_true
(Trebuchet.new(User.new(3)).launch('highly_experimental') { times += 1 }).should be_false
times.should == 1
end
it "should not blow up if block is missing" do
lambda do
Trebuchet.aim('highly_experimental', :users, [1,2])
Trebuchet.new(User.new(1)).launch('highly_experimental').should be_true
Trebuchet.new(User.new(3)).launch('highly_experimental').should be_false
Trebuchet.new(nil).launch('highly_experimental').should be_false
end.should_not raise_error(LocalJumpError)
end
end
describe "logging" do
before(:all) do
Trebuchet.aim('highly_experimental', :users, [1,2])
Trebuchet.aim('disused', :disabled)
end
before(:each) do
Trebuchet.initialize_logs
end
it "should log" do
Trebuchet.logs.should == {}
Trebuchet.new(User.new(1)).launch?('highly_experimental').should == true
Trebuchet.logs['highly_experimental'].should == true
end
it "should log false/nil" do
Trebuchet.logs['complely_fabricated'] == nil
Trebuchet.logs['disused'].should == nil
Trebuchet.new(User.new(1)).launch?('disused') #.should == false
Trebuchet.logs['disused'].should == false
end
it "it should clear logs" do
Trebuchet.new(User.new(1)).launch?('highly_experimental').should == true
Trebuchet.logs['highly_experimental'].should == true
Trebuchet.initialize_logs
Trebuchet.logs.should == {}
end
it "should log from multiple trebuchet instances" do
Trebuchet.new(User.new(1)).launch?('highly_experimental') #.should == true
Trebuchet.new(User.new(1)).launch?('disused') #.should == false
Trebuchet.new(nil).launch?('waste_of_time') #.should == false
Trebuchet.logs['highly_experimental'].should == true
Trebuchet.logs['disused'].should == false
Trebuchet.logs['waste_of_time'].should == false
end
end
describe "exception handling" do
before :all do
class BoomError < StandardError ; end
Trebuchet.define_strategy(:boom) do
raise BoomError.new "BOOM!"
end
Trebuchet.aim('optimism', :boom)
end
it "should swallow exceptions if no exception_handler defined" do
expect { Trebuchet.current.launch?("optimism") }.to_not raise_exception
end
it "should invoke exception_handler if defined" do
@feature = nil
@exception = nil
Trebuchet.exception_handler = lambda { |e, f, t| @exception = e; @feature = f }
Trebuchet.current.launch?("optimism")
@exception.should_not be_nil
@feature.should == 'optimism'
end
it "should allow exception_handler to raise the exception" do
Trebuchet.exception_handler = lambda { |e, f, t| raise e } # useful in development
expect { Trebuchet.current.launch?("optimism") }.to raise_exception(BoomError)
end
it "should accept 0 to 3 arguments" do
Trebuchet.exception_handler = lambda { @last_arg = eval local_variables.last.to_s }
Trebuchet.current.launch?("optimism")
@last_arg.should == nil
Trebuchet.exception_handler = lambda { |e| @last_arg = eval local_variables.last.to_s }
Trebuchet.current.launch?("optimism")
@last_arg.should be_a(StandardError)
Trebuchet.exception_handler = lambda { |e, f| @last_arg = eval local_variables.last.to_s }
Trebuchet.current.launch?("optimism")
@last_arg.should == "optimism"
Trebuchet.exception_handler = lambda { |e, f, t| @last_arg = eval local_variables.last.to_s }
Trebuchet.current.launch?("optimism")
@last_arg.should be_a(Trebuchet)
Trebuchet.exception_handler = lambda { |*args| @args = args }
Trebuchet.current.launch?("optimism")
@args.size.should == 3
@args.last.should be_a(Trebuchet)
end
it "should not blow up if exception_handler is not a proc" do
Trebuchet.exception_handler = "one of my shoes"
expect { Trebuchet.current.launch?("optimism") }.to_not raise_exception
Trebuchet.exception_handler = false
expect { Trebuchet.current.launch?("optimism") }.to_not raise_exception
Trebuchet.exception_handler = true
expect { Trebuchet.current.launch?("optimism") }.to_not raise_exception
Trebuchet.exception_handler = Trebuchet.current
expect { Trebuchet.current.launch?("optimism") }.to_not raise_exception
end
end
it "stores state variables" do
%i{author admin_view admin_edit time_zone}.each do |key|
begin
Trebuchet.public_send :"#{key}=", 1
expect(Trebuchet.public_send(key)).to eq 1
ensure
Trebuchet.public_send :"#{key}=", nil
end
end
end
end
================================================
FILE: spec/user.rb
================================================
class User < Struct.new(:id, :role)
def has_role?(role)
self.role == role
end
end
================================================
FILE: spec/user_id_strategy_spec.rb
================================================
require 'spec_helper'
describe Trebuchet::Strategy::UserId do
it "should only launch to designated users" do
Trebuchet.aim('highly_experimental', :users, [1, 2])
yes = [1, 2]
no = [3, 4]
yes.each do |n|
Trebuchet.new(User.new(n)).launch?('highly_experimental').should be_true
end
no.each do |n|
Trebuchet.new(User.new(n)).launch?('highly_experimental').should be_false
end
end
it "should always return booleans" do
Trebuchet.feature('time_machine').aim(:users, [1])
t = Trebuchet.new User.new(1)
t.launch?('time_machine').should === true
t = Trebuchet.new User.new(117)
t.launch?('time_machine').should === false
end
# this behavior should be deprecated -- causes problems with augment
it "should not break if one id is passed instead of an array" do
Trebuchet.feature('time_machine').aim(:users, 1)
t = Trebuchet.new User.new(1)
t.launch?('time_machine').should === true
end
it "should not break on missing user" do
Trebuchet.feature("the chosen ones").aim(:users, [1,2,3])
t = Trebuchet.new User.new(nil)
t.launch?("the chosen ones").should be_false
t = Trebuchet.new nil
t.launch?("the chosen ones").should be_false
end
end
================================================
FILE: spec/visitor_experiment_strategy_spec.rb
================================================
require 'spec_helper'
describe Trebuchet::Strategy::VisitorExperiment do
before do
@feature_name = "Infrared Vision"
@feature_name2 = "Alligator Tail"
@experiment_name = "Superhumanity"
@user = User.new(0)
@mock_request = mock_request('abcdef')
@trebuchet = Trebuchet.new(@user, @mock_request)
end
it "should match a user in a bucket" do
Trebuchet.aim(@feature_name, :visitor_experiment, :name => @experiment_name, :bucket => 1)
should_not_launch(@feature_name, (1..50).to_a) # should never launch without a request
# these values just happen to hash for the algorithm and experiment name
positive = [5, 14, 15, 198, 200, 549]
negative = [1, 2, 25, 550]
positive.each do |i|
Trebuchet.visitor_id = i
Trebuchet.feature(@feature_name).launch_at?(@user, @mock_request).should be_true
end
negative.each do |i|
Trebuchet.visitor_id = i
Trebuchet.feature(@feature_name).launch_at?(@user, @mock_request).should be_false
end
end
it "should launch nil request to no bucket" do
Trebuchet.aim(@feature_name, :visitor_experiment, :name => @experiment_name, :total_buckets => 2, :bucket => 1)
Trebuchet.aim(@feature_name2, :visitor_experiment, :name => @experiment_name, :total_buckets => 2, :bucket => 2)
Trebuchet.visitor_id = nil
@trebuchet.launch?(@feature_name).should be_false
@trebuchet.launch?(@feature_name2).should be_false
end
end
================================================
FILE: spec/visitor_percent_deprecated_strategy_spec.rb
================================================
require 'spec_helper'
describe Trebuchet::Strategy::VisitorPercentDeprecated do
it "should be deprecated" do
Trebuchet::Feature.with_deprecated_strategies_enabled(false) do
expect {
Trebuchet.aim('some_feature', :visitor_percent_deprecated, 5)
}.to raise_error(/deprecated/)
end
end
it "should not break if no visitor id is set" do
Trebuchet.aim('some_feature', :visitor_percent_deprecated, 100)
t = Trebuchet.new(User.new(0))
t.launch?('some_feature').should == false
end
it "should require a request" do
Trebuchet.visitor_id = 1
Trebuchet.aim('some_feature', :visitor_percent_deprecated, 100)
t = Trebuchet.new(User.new(1), nil)
t.launch?('some_feature').should == false
end
it "should not require a user" do
Trebuchet.visitor_id = 1
Trebuchet.aim('some_feature', :visitor_percent_deprecated, 100)
t = Trebuchet.new(nil, mock_request('12345'))
t.launch?('some_feature').should == true
end
describe 'visitor id integer' do
before do
Trebuchet.visitor_id = 123
end
def should_launch_test(feature_name)
# offset of some_feature is 33
Trebuchet.aim('some_feature', feature_name, 100)
offset = Trebuchet.feature('some_feature').strategy.offset
user = User.new(0)
request = mock_request('12345')
Trebuchet.feature('some_feature').launch_at?(user, request).should == true
visitor_id = Trebuchet.visitor_id.call
Trebuchet.aim('some_feature', feature_name, 91) # 33 + 91 includes 123 % 100
Trebuchet.feature('some_feature').launch_at?(user, request).should == true
Trebuchet.aim('some_feature', feature_name, 90)
Trebuchet.feature('some_feature').launch_at?(user, request).should == false
end
it 'should launch' do
should_launch_test(:visitor_percent_deprecated)
end
end
describe 'visitor id proc' do
before do
Trebuchet.visitor_id = proc { |request| request && request.cookies[:visitor] && request.cookies[:visitor].hash }
end
it 'should not launch if no request is present' do
Trebuchet.aim('some_feature', :visitor_percent_deprecated, 100)
should_not_launch('some_feature', [1000])
end
it 'should launch to a valid session' do
Trebuchet.aim('some_feature', :visitor_percent_deprecated, 100)
t = Trebuchet.new(User.new(0), mock_request('abcdef'))
t.launch?('some_feature').should == true
end
it 'should not launch to a nil session ID' do
Trebuchet.aim('some_feature', :visitor_percent_deprecated, 100)
t = Trebuchet.new(User.new(0), mock_request(nil))
t.launch?('some_feature').should == false
end
end
describe 'visitor id invalid' do
it "should handle nil" do
Trebuchet.visitor_id = nil
Trebuchet.aim('some_feature', :visitor_percent_deprecated, 100)
t = Trebuchet.new(User.new(0), mock_request('abcdef'))
t.launch?('some_feature').should == false
end
end
describe 'percentable' do
before do
@feature = Trebuchet.feature("liberty")
@user = User.new(0)
@request = mock_request('abcdef')
@trebuchet = Trebuchet.new(@user, @request)
end
it "should use from and to" do
@feature.aim(:visitor_percent_deprecated, :from => 5, :to => 10)
Trebuchet.feature("liberty").strategy.offset.should == 0
Trebuchet.visitor_id = 10
Trebuchet.feature("liberty").launch_at?(@user, @request).should == true
Trebuchet.visitor_id = 5
Trebuchet.feature("liberty").launch_at?(@user, @request).should == true
Trebuchet.visitor_id = 4
Trebuchet.feature("liberty").launch_at?(@user, @request).should == false
Trebuchet.visitor_id = 11
Trebuchet.feature("liberty").launch_at?(@user, @request).should == false
end
it "should use a percentage" do
@feature.aim(:visitor_percent_deprecated, :percentage => 25)
offset = @feature.strategy.offset
offset.should == 90
Trebuchet.visitor_id = 24 + offset
Trebuchet.feature("liberty").launch_at?(@user, @request).should == true
Trebuchet.visitor_id = 0 + offset
Trebuchet.feature("liberty").launch_at?(@user, @request).should == true
Trebuchet.visitor_id = 5 + offset
Trebuchet.feature("liberty").launch_at?(@user, @request).should == true
Trebuchet.visitor_id = 25 + offset
Trebuchet.feature("liberty").launch_at?(@user, @request).should == false
end
end
end
================================================
FILE: spec/visitor_percent_strategy_spec.rb
================================================
require 'spec_helper'
describe Trebuchet::Strategy::VisitorPercent do
it "should not break if no visitor id is set" do
Trebuchet.aim('some_feature', :visitor_percent, 100)
t = Trebuchet.new(User.new(0))
t.launch?('some_feature').should == false
end
it "should require a request" do
Trebuchet.visitor_id = 1
Trebuchet.aim('some_feature', :visitor_percent, 100)
t = Trebuchet.new(User.new(1), nil)
t.launch?('some_feature').should == false
end
it "should not require a user" do
Trebuchet.visitor_id = 1
Trebuchet.aim('some_feature', :visitor_percent, 100)
t = Trebuchet.new(nil, mock_request('12345'))
t.launch?('some_feature').should == true
end
describe 'visitor id integer' do
before do
Trebuchet.visitor_id = 123
end
def should_launch_test(feature_name)
end
it 'should launch' do
Trebuchet.aim('some_feature', :visitor_percent, 100)
user = User.new(0)
request = mock_request('12345')
Trebuchet.feature('some_feature').launch_at?(user, request).should == true
visitor_id = Trebuchet.visitor_id.call
Trebuchet.aim('some_feature', :visitor_percent, 91)
Trebuchet.feature('some_feature').launch_at?(user, request).should == true
Trebuchet.aim('some_feature', :visitor_percent, 10)
Trebuchet.feature('some_feature').launch_at?(user, request).should == false
end
end
describe 'visitor id proc' do
before do
Trebuchet.visitor_id = proc { |request| request && request.cookies[:visitor] && request.cookies[:visitor].hash }
end
it 'should not launch if no request is present' do
Trebuchet.aim('some_feature', :visitor_percent, 100)
should_not_launch('some_feature', [1000])
end
it 'should launch to a valid session' do
Trebuchet.aim('some_feature', :visitor_percent, 100)
t = Trebuchet.new(User.new(0), mock_request('abcdef'))
t.launch?('some_feature').should == true
end
it 'should not launch to a nil session ID' do
Trebuchet.aim('some_feature', :visitor_percent, 100)
t = Trebuchet.new(User.new(0), mock_request(nil))
t.launch?('some_feature').should == false
end
end
describe 'visitor id invalid' do
it "should handle nil" do
Trebuchet.visitor_id = nil
Trebuchet.aim('some_feature', :visitor_percent, 100)
t = Trebuchet.new(User.new(0), mock_request('abcdef'))
t.launch?('some_feature').should == false
end
end
end
================================================
FILE: trebuchet.gemspec
================================================
# -*- encoding: utf-8 -*-
$:.push File.expand_path("../lib", __FILE__)
require 'trebuchet'
Gem::Specification.new do |s|
s.name = "trebuchet"
s.version = Trebuchet::VERSION
s.platform = Gem::Platform::RUBY
s.authors = ["Justin Jones", "Tobi Knaup", "Ross Allen"]
s.email = ["justin@airbnb.com"]
s.homepage = "http://www.airbnb.com"
s.summary = %q{Trebuchet launches features at people}
s.description = %q{Wisely choose a strategy, aim, and launch!}
s.files = `git ls-files`.split("\n")
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
s.require_paths = ["lib"]
# redis and memcache are optional
s.add_dependency 'json'
s.add_development_dependency 'rspec', '~> 2.12.0'
s.add_development_dependency 'mock_redis', '~> 0.6.5'
s.add_development_dependency 'rake', '>= 10.0.3'
end
gitextract_xbvggvpr/ ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Gemfile ├── README.md ├── Rakefile ├── app/ │ ├── controllers/ │ │ └── trebuchet_rails/ │ │ └── features_controller.rb │ ├── helpers/ │ │ └── trebuchet_helper.rb │ └── views/ │ ├── layouts/ │ │ └── trebuchet.html.erb │ └── trebuchet_rails/ │ ├── features/ │ │ ├── index.html.erb │ │ └── timeline.html.erb │ └── trebuchet.css ├── config/ │ └── routes.rb ├── init.rb ├── lib/ │ ├── trebuchet/ │ │ ├── action_controller.rb │ │ ├── action_controller_filter.rb │ │ ├── backend/ │ │ │ ├── disabled.rb │ │ │ ├── memcached.rb │ │ │ ├── memory.rb │ │ │ ├── redis.rb │ │ │ ├── redis_cached.rb │ │ │ └── redis_hammerspaced.rb │ │ ├── backend.rb │ │ ├── error.rb │ │ ├── feature/ │ │ │ └── stubbing.rb │ │ ├── feature.rb │ │ ├── state.rb │ │ ├── strategy/ │ │ │ ├── base.rb │ │ │ ├── custom.rb │ │ │ ├── custom_request_aware.rb │ │ │ ├── default.rb │ │ │ ├── everyone.rb │ │ │ ├── experiment.rb │ │ │ ├── hostname.rb │ │ │ ├── invalid.rb │ │ │ ├── logic_and.rb │ │ │ ├── logic_base.rb │ │ │ ├── logic_not.rb │ │ │ ├── logic_or.rb │ │ │ ├── multiple.rb │ │ │ ├── nobody.rb │ │ │ ├── per_denomination.rb │ │ │ ├── percent.rb │ │ │ ├── percent_deprecated.rb │ │ │ ├── stub.rb │ │ │ ├── user_id.rb │ │ │ ├── visitor_experiment.rb │ │ │ ├── visitor_percent.rb │ │ │ └── visitor_percent_deprecated.rb │ │ ├── strategy.rb │ │ └── version.rb │ ├── trebuchet.rb │ ├── trebuchet_rails/ │ │ └── engine.rb │ └── trebuchet_rails.rb ├── spec/ │ ├── custom_request_aware_strategy_spec.rb │ ├── custom_strategy_spec.rb │ ├── default_strategy_spec.rb │ ├── disabled_backend_spec.rb │ ├── everyone_strategy_spec.rb │ ├── experiment_strategy_spec.rb │ ├── feature_spec.rb │ ├── logic_and_strategy_spec.rb │ ├── logic_base_strategy_spec.rb │ ├── logic_not_strategy_spec.rb │ ├── logic_or_strategy_spec.rb │ ├── multiple_strategy_spec.rb │ ├── nobody_strategy_spec.rb │ ├── per_denomination_strategy_spec.rb │ ├── percent_deprecated_strategy_spec.rb │ ├── percent_strategy_spec.rb │ ├── redis_backend_spec.rb │ ├── redis_hammerspaced_spec.rb │ ├── spec_helper.rb │ ├── stubbing_spec.rb │ ├── trebuchet_spec.rb │ ├── user.rb │ ├── user_id_strategy_spec.rb │ ├── visitor_experiment_strategy_spec.rb │ ├── visitor_percent_deprecated_strategy_spec.rb │ └── visitor_percent_strategy_spec.rb └── trebuchet.gemspec
SYMBOL INDEX (309 symbols across 48 files)
FILE: app/controllers/trebuchet_rails/features_controller.rb
type TrebuchetRails (line 1) | module TrebuchetRails
class FeaturesController (line 3) | class FeaturesController < ApplicationController
method index (line 14) | def index
method timeline (line 27) | def timeline
method control_access (line 52) | def control_access
method get_time_zone (line 65) | def get_time_zone
FILE: app/helpers/trebuchet_helper.rb
type TrebuchetHelper (line 1) | module TrebuchetHelper
function feature (line 3) | def feature(feature)
function strategy (line 8) | def strategy(strategy)
function trebuchet_css (line 18) | def trebuchet_css
FILE: lib/trebuchet.rb
class Trebuchet (line 3) | class Trebuchet
method backend (line 18) | def backend
method set_backend (line 23) | def set_backend(backend_type, *args)
method initialize_logs (line 40) | def initialize_logs
method log (line 44) | def log(feature_name, result)
method logs (line 49) | def logs
method current= (line 53) | def current=(other)
method current (line 57) | def current
method reset_current! (line 62) | def reset_current!
method thread_local_key (line 66) | def thread_local_key
method state (line 75) | def state
method state= (line 83) | def state=(new_state)
method threadsafe_state? (line 91) | def threadsafe_state?
method aim (line 96) | def self.aim(feature_name, *args)
method dismantle (line 100) | def self.dismantle(feature_name)
method dismantle_stubs (line 104) | def self.dismantle_stubs
method define_strategy (line 108) | def self.define_strategy(name, &block)
method define_request_aware_strategy (line 112) | def self.define_request_aware_strategy(name, &block)
method visitor_id= (line 116) | def self.visitor_id=(id_or_proc)
method use_with_rails! (line 126) | def self.use_with_rails!
method feature (line 132) | def self.feature(name)
method initialize (line 136) | def initialize(current_user, request = nil)
method launch (line 142) | def launch(feature, &block)
method launch? (line 148) | def launch?(feature)
method handle_exception (line 163) | def handle_exception(exception, feature = nil)
method export (line 171) | def self.export
method history (line 179) | def self.history(include_archived = false)
FILE: lib/trebuchet/action_controller.rb
type Trebuchet::ActionController (line 3) | module Trebuchet::ActionController
function included (line 5) | def self.included(base)
function trebuchet (line 12) | def trebuchet
FILE: lib/trebuchet/action_controller_filter.rb
class Trebuchet::ActionControllerFilter (line 1) | class Trebuchet::ActionControllerFilter
method before (line 3) | def self.before(controller)
method after (line 15) | def self.after(controller)
FILE: lib/trebuchet/backend.rb
type Trebuchet::Backend (line 1) | module Trebuchet::Backend
function lookup (line 3) | def self.lookup(name)
FILE: lib/trebuchet/backend/disabled.rb
class Trebuchet::Backend::Disabled (line 4) | class Trebuchet::Backend::Disabled
method initialize (line 6) | def initialize(*args)
method get_strategy (line 9) | def get_strategy(feature_name)
method set_strategy (line 13) | def set_strategy(feature, strategy, options = nil)
method append_strategy (line 17) | def append_strategy(feature, strategy, options = nil)
method get_feature_names (line 21) | def get_feature_names
FILE: lib/trebuchet/backend/memcached.rb
class Trebuchet::Backend::Memcached (line 3) | class Trebuchet::Backend::Memcached
method initialize (line 7) | def initialize(*args)
method get_strategy (line 12) | def get_strategy(feature_name)
method set_strategy (line 16) | def set_strategy(feature, strategy, options = nil)
method append_strategy (line 20) | def append_strategy(feature, strategy, options = nil)
method get_feature_names (line 24) | def get_feature_names
method key (line 30) | def key(feature_name)
FILE: lib/trebuchet/backend/memory.rb
class Trebuchet::Backend::Memory (line 1) | class Trebuchet::Backend::Memory
method initialize (line 3) | def initialize(*args)
method get_strategy (line 8) | def get_strategy(feature_name)
method set_strategy (line 12) | def set_strategy(feature, strategy, options = nil)
method append_strategy (line 17) | def append_strategy(feature, strategy, options = nil)
method remove_strategy (line 28) | def remove_strategy(feature)
method get_feature_names (line 32) | def get_feature_names
method remove_feature (line 36) | def remove_feature(feature)
method get_archived_feature_names (line 42) | def get_archived_feature_names
FILE: lib/trebuchet/backend/redis.rb
class Trebuchet::Backend::Redis (line 4) | class Trebuchet::Backend::Redis
method initialize (line 8) | def initialize(*args)
method get_strategy (line 27) | def get_strategy(feature_name)
method unpack_strategy (line 32) | def unpack_strategy(options)
method set_strategy (line 48) | def set_strategy(feature_name, strategy, options = nil)
method append_strategy (line 54) | def append_strategy(feature_name, strategy, options = nil)
method remove_strategy (line 62) | def remove_strategy(feature_name)
method get_feature_names (line 67) | def get_feature_names
method get_archived_feature_names (line 71) | def get_archived_feature_names
method remove_feature (line 75) | def remove_feature(feature_name)
method store_history (line 82) | def store_history(feature_name)
method get_history (line 89) | def get_history(feature_name)
method get_all_history (line 98) | def get_all_history(include_archived = false)
method update_sentinel (line 120) | def update_sentinel
method get_sentinel (line 124) | def get_sentinel
method archived_feature_names_key (line 130) | def archived_feature_names_key
method feature_names_key (line 134) | def feature_names_key
method feature_key (line 138) | def feature_key(feature_name)
method feature_history_key (line 142) | def feature_history_key(feature_name, timestamp = nil)
method sentinel_key (line 148) | def sentinel_key
FILE: lib/trebuchet/backend/redis_cached.rb
class Trebuchet::Backend::RedisCached (line 3) | class Trebuchet::Backend::RedisCached < Trebuchet::Backend::Redis
method get_strategy (line 7) | def get_strategy(feature_name)
method append_strategy (line 19) | def append_strategy(feature_name, strategy, options = nil)
method cache_strategy (line 26) | def cache_strategy(feature_name, strategy)
method cached_strategies (line 30) | def cached_strategies
method cache_cleared_at (line 34) | def cache_cleared_at
method clear_cached_strategies (line 38) | def clear_cached_strategies
method refresh (line 43) | def refresh
FILE: lib/trebuchet/backend/redis_hammerspaced.rb
class Trebuchet::Backend::RedisHammerspaced (line 4) | class Trebuchet::Backend::RedisHammerspaced < Trebuchet::Backend::Redis
method initialize (line 13) | def initialize(*args)
method get_strategy (line 19) | def get_strategy(feature_name)
method get_strategy_hammerspace (line 29) | def get_strategy_hammerspace(feature_name)
method unpack_strategy_hammerspace (line 42) | def unpack_strategy_hammerspace(options)
method get_feature_names (line 59) | def get_feature_names
method append_strategy (line 65) | def append_strategy(feature_name, strategy, options = nil)
method cache_strategy (line 72) | def cache_strategy(feature_name, strategy)
method cached_strategies (line 76) | def cached_strategies
method clear_cached_strategies (line 80) | def clear_cached_strategies
method refresh (line 84) | def refresh
method update_hammerspace (line 93) | def update_hammerspace(forced = false)
method generate_hammerspace_hash (line 117) | def generate_hammerspace_hash(feature_names, features, last_updated)
FILE: lib/trebuchet/error.rb
class Trebuchet::Error (line 1) | class Trebuchet::Error < StandardError ; end
class Trebuchet::BackendInitializationError (line 2) | class Trebuchet::BackendInitializationError < Trebuchet::Error ; end
class Trebuchet::BackendError (line 3) | class Trebuchet::BackendError < Trebuchet::Error ; end
FILE: lib/trebuchet/feature.rb
class Trebuchet::Feature (line 3) | class Trebuchet::Feature
method initialize (line 11) | def initialize(name)
method find (line 15) | def self.find(name)
method reset (line 27) | def reset
method all (line 31) | def self.all
method dismantled (line 35) | def self.dismantled
method exist? (line 39) | def self.exist?(name)
method with_deprecated_strategies_enabled (line 46) | def self.with_deprecated_strategies_enabled(value=true, &block)
method strategy (line 56) | def strategy
method valid? (line 60) | def valid?
method launch_at? (line 64) | def launch_at?(user, request = nil)
method aim (line 70) | def aim(strategy_name, options = nil)
method adjust (line 85) | def adjust(strategy_name, options = nil)
method augment (line 91) | def augment(strategy_name, new_options)
method dismantle (line 112) | def dismantle
method add_comment (line 117) | def add_comment(comment)
method expiration_date (line 125) | def expiration_date
method set_expiration_date (line 131) | def set_expiration_date(expiration_date)
method history (line 136) | def history
method feature_id (line 143) | def feature_id
method as_json (line 151) | def as_json(options = {})
method to_s (line 155) | def to_s
method inspect (line 160) | def inspect
method export (line 164) | def export
method chained? (line 170) | def chained?
FILE: lib/trebuchet/feature/stubbing.rb
class Trebuchet (line 1) | class Trebuchet
class Feature (line 2) | class Feature
type Stubbing (line 3) | module Stubbing
function stub (line 5) | def stub(state)
function stubbed? (line 9) | def stubbed?
function included (line 13) | def self.included(base)
type ClassMethods (line 17) | module ClassMethods
function dismantle_stubs (line 19) | def dismantle_stubs
function stubbed_features (line 23) | def stubbed_features
FILE: lib/trebuchet/strategy.rb
type Trebuchet::Strategy (line 3) | module Trebuchet::Strategy
function for_feature (line 5) | def self.for_feature(feature)
function find (line 15) | def self.find(*args)
function name_class_map (line 41) | def self.name_class_map
function deprecated_strategy_names (line 63) | def self.deprecated_strategy_names
function class_for_name (line 70) | def self.class_for_name(name)
function name_for_class (line 75) | def self.name_for_class(klass)
type PerDenominationable (line 80) | module PerDenominationable
function initialize (line 83) | def initialize(options)
function set_range_from_options (line 88) | def set_range_from_options(options)
function value_in_range? (line 101) | def value_in_range?(value)
function to_s (line 107) | def to_s
function export (line 111) | def export
type Percentable (line 118) | module Percentable
function set_range_from_options (line 124) | def set_range_from_options(options)
function to_s (line 135) | def to_s
function export (line 141) | def export
type PercentableDeprecated (line 151) | module PercentableDeprecated
function initialize (line 153) | def initialize(options)
function set_range_from_options (line 158) | def set_range_from_options(options)
function offset (line 177) | def offset
function percentage (line 185) | def percentage
function value_in_range? (line 192) | def value_in_range?(value)
function offset_from (line 201) | def offset_from
function offset_to (line 205) | def offset_to
function to_s (line 209) | def to_s
function export (line 224) | def export
type Experimentable (line 234) | module Experimentable
function initialize_experiment (line 238) | def initialize_experiment(options)
function value_in_bucket? (line 245) | def value_in_bucket?(value)
function valid? (line 253) | def valid?
function type (line 259) | def type
function as_json (line 263) | def as_json(options = {})
function to_s (line 272) | def to_s
function export (line 277) | def export
function inspect (line 281) | def inspect
FILE: lib/trebuchet/strategy/base.rb
class Trebuchet::Strategy::Base (line 3) | class Trebuchet::Strategy::Base
method name (line 7) | def name
method feature_id (line 11) | def feature_id
method needs_user? (line 15) | def needs_user?
method strategy_name (line 19) | def self.strategy_name
method as_json (line 23) | def as_json(options = {})
method inspect (line 33) | def inspect
method export (line 37) | def export(options = nil)
FILE: lib/trebuchet/strategy/custom.rb
class Trebuchet::Strategy::Custom (line 1) | class Trebuchet::Strategy::Custom < Trebuchet::Strategy::Base
method initialize (line 7) | def initialize(name, options = nil)
method launch_at? (line 13) | def launch_at?(user, request = nil)
method define (line 17) | def self.define(name, block)
method exists? (line 21) | def self.exists?(name)
method needs_user? (line 25) | def needs_user?
method as_json (line 35) | def as_json(options = {})
method to_s (line 39) | def to_s
method export (line 43) | def export
FILE: lib/trebuchet/strategy/custom_request_aware.rb
class Trebuchet::Strategy::CustomRequestAware (line 1) | class Trebuchet::Strategy::CustomRequestAware < Trebuchet::Strategy::Custom
method initialize (line 4) | def initialize(name, options = nil)
method define (line 10) | def self.define(name, block)
method exists? (line 14) | def self.exists?(name)
method launch_at? (line 18) | def launch_at?(user, request = nil)
method to_s (line 23) | def to_s
FILE: lib/trebuchet/strategy/default.rb
class Trebuchet::Strategy::Default (line 4) | class Trebuchet::Strategy::Default < Trebuchet::Strategy::Base
method initialize (line 7) | def initialize(options = nil)
method name (line 11) | def name
method launch_at? (line 15) | def launch_at?(user, request = nil)
method needs_user? (line 19) | def needs_user?
method to_s (line 23) | def to_s
FILE: lib/trebuchet/strategy/everyone.rb
class Trebuchet::Strategy::Everyone (line 3) | class Trebuchet::Strategy::Everyone < Trebuchet::Strategy::Base
method initialize (line 6) | def initialize(options = nil)
method name (line 10) | def name
method launch_at? (line 14) | def launch_at?(user, request = nil)
method needs_user? (line 18) | def needs_user?
method to_s (line 22) | def to_s
FILE: lib/trebuchet/strategy/experiment.rb
class Trebuchet::Strategy::Experiment (line 3) | class Trebuchet::Strategy::Experiment < Trebuchet::Strategy::Base
method initialize (line 7) | def initialize(options = {})
method launch_at? (line 11) | def launch_at?(user, request = nil)
FILE: lib/trebuchet/strategy/hostname.rb
class Trebuchet::Strategy::Hostname (line 1) | class Trebuchet::Strategy::Hostname < Trebuchet::Strategy::Base
method initialize (line 5) | def initialize(hostnames)
method launch_at? (line 13) | def launch_at?(user, request = nil)
method needs_user? (line 18) | def needs_user?
method to_s (line 22) | def to_s
method export (line 26) | def export
FILE: lib/trebuchet/strategy/invalid.rb
class Trebuchet::Strategy::Invalid (line 2) | class Trebuchet::Strategy::Invalid < Trebuchet::Strategy::Base
method initialize (line 6) | def initialize(name, options = nil)
method name (line 11) | def name
method launch_at? (line 15) | def launch_at?(user, request = nil)
method needs_user? (line 19) | def needs_user?
method to_s (line 23) | def to_s
FILE: lib/trebuchet/strategy/logic_and.rb
class Trebuchet::Strategy::LogicAnd (line 3) | class Trebuchet::Strategy::LogicAnd < Trebuchet::Strategy::LogicBase
method launch_at? (line 5) | def launch_at?(user, request = nil)
FILE: lib/trebuchet/strategy/logic_base.rb
class Trebuchet::Strategy::LogicBase (line 1) | class Trebuchet::Strategy::LogicBase < Trebuchet::Strategy::Base
method initialize (line 6) | def initialize(options = {})
method feature= (line 15) | def feature=(f)
method launch_at? (line 20) | def launch_at?(user, request = nil)
method needs_user? (line 24) | def needs_user?
FILE: lib/trebuchet/strategy/logic_not.rb
class Trebuchet::Strategy::LogicNot (line 3) | class Trebuchet::Strategy::LogicNot < Trebuchet::Strategy::LogicBase
method launch_at? (line 5) | def launch_at?(user, request = nil)
FILE: lib/trebuchet/strategy/logic_or.rb
class Trebuchet::Strategy::LogicOr (line 3) | class Trebuchet::Strategy::LogicOr < Trebuchet::Strategy::LogicBase
method launch_at? (line 5) | def launch_at?(user, request = nil)
FILE: lib/trebuchet/strategy/multiple.rb
class Trebuchet::Strategy::Multiple (line 1) | class Trebuchet::Strategy::Multiple < Trebuchet::Strategy::Base
method initialize (line 5) | def initialize(args)
method feature= (line 13) | def feature=(f)
method launch_at? (line 18) | def launch_at?(user, request = nil)
method as_json (line 23) | def as_json(options = {})
method needs_user? (line 27) | def needs_user?
method export (line 33) | def export
FILE: lib/trebuchet/strategy/nobody.rb
class Trebuchet::Strategy::Nobody (line 3) | class Trebuchet::Strategy::Nobody < Trebuchet::Strategy::Base
method initialize (line 6) | def initialize(options = nil)
method name (line 10) | def name
method launch_at? (line 14) | def launch_at?(user, request = nil)
method needs_user? (line 18) | def needs_user?
method to_s (line 22) | def to_s
FILE: lib/trebuchet/strategy/per_denomination.rb
class Trebuchet::Strategy::PerDenomination (line 1) | class Trebuchet::Strategy::PerDenomination < Trebuchet::Strategy::Base
method set_range_from_options (line 4) | def set_range_from_options(options = {})
method launch_at? (line 11) | def launch_at?(user, request = nil)
FILE: lib/trebuchet/strategy/percent.rb
class Trebuchet::Strategy::Percent (line 1) | class Trebuchet::Strategy::Percent < Trebuchet::Strategy::Base
method initialize (line 5) | def initialize(options)
method launch_at? (line 9) | def launch_at?(user, request = nil)
FILE: lib/trebuchet/strategy/percent_deprecated.rb
class Trebuchet::Strategy::PercentDeprecated (line 1) | class Trebuchet::Strategy::PercentDeprecated < Trebuchet::Strategy::Base
method initialize (line 5) | def initialize(options)
method launch_at? (line 9) | def launch_at?(user, request = nil)
FILE: lib/trebuchet/strategy/stub.rb
class Trebuchet (line 1) | class Trebuchet
type Strategy (line 2) | module Strategy
class Stub (line 4) | class Stub < Trebuchet::Strategy::Base
method initialize (line 7) | def initialize(state)
method launch_at? (line 11) | def launch_at?(user, request = nil)
method needs_user? (line 15) | def needs_user?
method to_s (line 19) | def to_s
method export (line 23) | def export
FILE: lib/trebuchet/strategy/user_id.rb
class Trebuchet::Strategy::UserId (line 1) | class Trebuchet::Strategy::UserId < Trebuchet::Strategy::Base
method initialize (line 5) | def initialize(user_ids)
method launch_at? (line 10) | def launch_at?(user, request = nil)
method to_s (line 14) | def to_s
method export (line 18) | def export
FILE: lib/trebuchet/strategy/visitor_experiment.rb
class Trebuchet::Strategy::VisitorExperiment (line 1) | class Trebuchet::Strategy::VisitorExperiment < Trebuchet::Strategy::Base
method initialize (line 5) | def initialize(options = {})
method launch_at? (line 9) | def launch_at?(user, request = nil)
method needs_user? (line 19) | def needs_user?
FILE: lib/trebuchet/strategy/visitor_percent.rb
class Trebuchet::Strategy::VisitorPercent (line 1) | class Trebuchet::Strategy::VisitorPercent < Trebuchet::Strategy::Base
method initialize (line 5) | def initialize(options)
method launch_at? (line 9) | def launch_at?(user, request = nil)
method needs_user? (line 20) | def needs_user?
FILE: lib/trebuchet/strategy/visitor_percent_deprecated.rb
class Trebuchet::Strategy::VisitorPercentDeprecated (line 1) | class Trebuchet::Strategy::VisitorPercentDeprecated < Trebuchet::Strateg...
method initialize (line 5) | def initialize(options)
method launch_at? (line 9) | def launch_at?(user, request = nil)
method needs_user? (line 20) | def needs_user?
FILE: lib/trebuchet/version.rb
class Trebuchet (line 1) | class Trebuchet
FILE: lib/trebuchet_rails/engine.rb
type TrebuchetRails (line 5) | module TrebuchetRails
class Engine (line 6) | class Engine < Rails::Engine
FILE: spec/feature_spec.rb
function feature (line 5) | def feature
function feature_names (line 9) | def feature_names
function archived_feature_names (line 13) | def archived_feature_names
FILE: spec/percent_deprecated_strategy_spec.rb
function should_only_launch_to_a_percentage_of_users (line 19) | def should_only_launch_to_a_percentage_of_users(feature_name)
function offset_ids (line 60) | def offset_ids(ids, offset)
FILE: spec/redis_hammerspaced_spec.rb
function uid (line 58) | def hammerspace.uid
function close (line 62) | def hammerspace.close
FILE: spec/spec_helper.rb
class Redis (line 9) | class Redis < MockRedis ; end
function should_launch (line 26) | def should_launch(feature, users)
function should_not_launch (line 30) | def should_not_launch(feature, users)
function should_or_should_not_launch (line 34) | def should_or_should_not_launch(feature, users, be_true_or_false)
function mock_request (line 41) | def mock_request(cookie = nil)
FILE: spec/trebuchet_spec.rb
class BoomError (line 96) | class BoomError < StandardError ; end
FILE: spec/user.rb
class User (line 1) | class User < Struct.new(:id, :role)
method has_role? (line 3) | def has_role?(role)
FILE: spec/visitor_percent_deprecated_strategy_spec.rb
function should_launch_test (line 38) | def should_launch_test(feature_name)
FILE: spec/visitor_percent_strategy_spec.rb
function should_launch_test (line 30) | def should_launch_test(feature_name)
Condensed preview — 81 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (125K chars).
[
{
"path": ".gitignore",
"chars": 46,
"preview": "*.gem\n.bundle\nGemfile.lock\npkg/*\n.idea\n.rspec\n"
},
{
"path": ".travis.yml",
"chars": 156,
"preview": "language: ruby\ncache: bundler\nsudo: false\nrvm:\n - 1.9.3\n - 2.1\n - 2.2\n - 2.3\n - 2.4\n - 2.5\n - 2.6\n\nbefore_install"
},
{
"path": "CHANGELOG.md",
"chars": 1979,
"preview": "## 0.12.0 (Nov 26, 2019)\n - Contain fiber-unsafe state of the main Trebuchet class into a state object and optionally st"
},
{
"path": "Gemfile",
"chars": 209,
"preview": "source \"http://rubygems.org\"\n\n# Specify your gem's dependencies in trebuchet.gemspec\ngemspec\n\ncurrent_ruby = Gem::Versio"
},
{
"path": "README.md",
"chars": 3626,
"preview": "Trebuchet\n=========\n\nTrebuchet launches features at people. Wisely choose a strategy, aim, and launch!\n\nInstallation\n---"
},
{
"path": "Rakefile",
"chars": 117,
"preview": "require 'bundler'\nBundler::GemHelper.install_tasks\n\ntask :spec do\n system 'rspec ./spec'\nend\n\ntask :default => :spec"
},
{
"path": "app/controllers/trebuchet_rails/features_controller.rb",
"chars": 2046,
"preview": "module TrebuchetRails\n\n class FeaturesController < ApplicationController\n\n if Rails::VERSION::MAJOR >= 5\n befor"
},
{
"path": "app/helpers/trebuchet_helper.rb",
"chars": 589,
"preview": "module TrebuchetHelper\n\n def feature(feature)\n \"<dd>#{feature.name}</dd>\n <dt><ul>#{strategy feature.strategy}</u"
},
{
"path": "app/views/layouts/trebuchet.html.erb",
"chars": 179,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n \t<title>Trebuchet</title>\n \t<style type=\"text/css\">\n \t\t<%= trebuchet_css %>\n \t</sty"
},
{
"path": "app/views/trebuchet_rails/features/index.html.erb",
"chars": 2263,
"preview": "<div id=\"trebuchet\">\n\t<h1>Trebuchet Admin</h1>\n\t<table class=\"treb-features\">\n\t\t<thead>\n\t\t\t<tr>\n\t\t\t\t<th>Feature Name</th"
},
{
"path": "app/views/trebuchet_rails/features/timeline.html.erb",
"chars": 474,
"preview": "<div id=\"trebuchet\">\n\t<h1>Trebuchet Timeline</h1>\n\t<table class=\"treb-features\">\n\t\t<thead>\n\t\t\t<tr>\n\t\t\t <th style=\"min-w"
},
{
"path": "app/views/trebuchet_rails/trebuchet.css",
"chars": 1312,
"preview": "#trebuchet {\n\tfont-family: HelveticaNeue, 'Helvetica Neue', HelveticaNeueRoman, HelveticaNeue-Roman, 'Helvetica Neue Rom"
},
{
"path": "config/routes.rb",
"chars": 437,
"preview": "# TrebuchetRails::Engine.routes do\n\nroutes_block = lambda do\n scope \"trebuchet\", :module => \"trebuchet_rails\" do\n ge"
},
{
"path": "init.rb",
"chars": 609,
"preview": "require 'trebuchet'\n\n# FIXME: happens too early\n# def set_trebuchet_namespace(app_name)\n# if Trebuchet.backend.respond"
},
{
"path": "lib/trebuchet/action_controller.rb",
"chars": 283,
"preview": "require 'trebuchet/action_controller_filter'\n\nmodule Trebuchet::ActionController\n\n def self.included(base)\n base.hel"
},
{
"path": "lib/trebuchet/action_controller_filter.rb",
"chars": 436,
"preview": "class Trebuchet::ActionControllerFilter\n\n def self.before(controller)\n Trebuchet.initialize_logs\n\n if Trebuchet.b"
},
{
"path": "lib/trebuchet/backend/disabled.rb",
"chars": 520,
"preview": "# This backend stores nothing and returns empty/false data (launch? will always be false)\n# It can be used to disable al"
},
{
"path": "lib/trebuchet/backend/memcached.rb",
"chars": 660,
"preview": "require 'memcache'\n\nclass Trebuchet::Backend::Memcached\n\n attr_accessor :namespace\n\n def initialize(*args)\n @memcac"
},
{
"path": "lib/trebuchet/backend/memory.rb",
"chars": 959,
"preview": "class Trebuchet::Backend::Memory\n\n def initialize(*args)\n @hash = {}\n @archived = []\n end\n\n def get_strategy(fe"
},
{
"path": "lib/trebuchet/backend/redis.rb",
"chars": 4095,
"preview": "require 'redis' unless defined?(Redis)\nrequire 'json'\n\nclass Trebuchet::Backend::Redis\n\n attr_accessor :namespace\n\n de"
},
{
"path": "lib/trebuchet/backend/redis_cached.rb",
"chars": 1290,
"preview": "require 'trebuchet/backend/redis'\n\nclass Trebuchet::Backend::RedisCached < Trebuchet::Backend::Redis\n\n # cache strategi"
},
{
"path": "lib/trebuchet/backend/redis_hammerspaced.rb",
"chars": 3683,
"preview": "require 'trebuchet/backend/redis'\nrequire 'json'\n\nclass Trebuchet::Backend::RedisHammerspaced < Trebuchet::Backend::Redi"
},
{
"path": "lib/trebuchet/backend.rb",
"chars": 345,
"preview": "module Trebuchet::Backend\n\n def self.lookup(name)\n # From ActiveSupport::Inflector.camelize\n const_name = name.to"
},
{
"path": "lib/trebuchet/error.rb",
"chars": 168,
"preview": "class Trebuchet::Error < StandardError ; end\nclass Trebuchet::BackendInitializationError < Trebuchet::Error ; end\nclass "
},
{
"path": "lib/trebuchet/feature/stubbing.rb",
"chars": 492,
"preview": "class Trebuchet\n class Feature\n module Stubbing\n\n def stub(state)\n self.class.stubbed_features[name] = s"
},
{
"path": "lib/trebuchet/feature.rb",
"chars": 4488,
"preview": "require 'trebuchet/feature/stubbing'\n\nclass Trebuchet::Feature\n include Stubbing\n\n @@deprecated_strategies_enabled = t"
},
{
"path": "lib/trebuchet/state.rb",
"chars": 202,
"preview": "# Represents the internal, global and thread-unsafe state of Trebuchet\nTrebuchet::State = Struct.new(\n :visitor_id, :cu"
},
{
"path": "lib/trebuchet/strategy/base.rb",
"chars": 674,
"preview": "require 'digest/sha1'\n\nclass Trebuchet::Strategy::Base\n\n attr_accessor :feature\n\n def name\n self.class.strategy_nam"
},
{
"path": "lib/trebuchet/strategy/custom.rb",
"chars": 932,
"preview": "class Trebuchet::Strategy::Custom < Trebuchet::Strategy::Base\n\n attr_reader :options, :custom_name\n\n @@custom_strategi"
},
{
"path": "lib/trebuchet/strategy/custom_request_aware.rb",
"chars": 680,
"preview": "class Trebuchet::Strategy::CustomRequestAware < Trebuchet::Strategy::Custom\n @@custom_request_aware_strategies = {}\n\n "
},
{
"path": "lib/trebuchet/strategy/default.rb",
"chars": 386,
"preview": "require 'singleton'\n\n# Default is to not launch the feature to anyone\nclass Trebuchet::Strategy::Default < Trebuchet::St"
},
{
"path": "lib/trebuchet/strategy/everyone.rb",
"chars": 383,
"preview": "require 'singleton'\n# Everyone is to launch the feature to everyone\nclass Trebuchet::Strategy::Everyone < Trebuchet::Str"
},
{
"path": "lib/trebuchet/strategy/experiment.rb",
"chars": 404,
"preview": "# require 'digest/sha1'\n\nclass Trebuchet::Strategy::Experiment < Trebuchet::Strategy::Base\n \n include Trebuchet::Strat"
},
{
"path": "lib/trebuchet/strategy/hostname.rb",
"chars": 508,
"preview": "class Trebuchet::Strategy::Hostname < Trebuchet::Strategy::Base\n\n attr_reader :hostnames\n\n def initialize(hostnames)\n "
},
{
"path": "lib/trebuchet/strategy/invalid.rb",
"chars": 453,
"preview": "# Default is to not launch the feature to anyone\nclass Trebuchet::Strategy::Invalid < Trebuchet::Strategy::Base\n\n attr_"
},
{
"path": "lib/trebuchet/strategy/logic_and.rb",
"chars": 257,
"preview": "require 'trebuchet/strategy/logic_base'\n\nclass Trebuchet::Strategy::LogicAnd < Trebuchet::Strategy::LogicBase\n\n def lau"
},
{
"path": "lib/trebuchet/strategy/logic_base.rb",
"chars": 646,
"preview": "class Trebuchet::Strategy::LogicBase < Trebuchet::Strategy::Base\n\n attr_reader :strategies\n attr_reader :options\n\n de"
},
{
"path": "lib/trebuchet/strategy/logic_not.rb",
"chars": 258,
"preview": "require 'trebuchet/strategy/logic_base'\n\nclass Trebuchet::Strategy::LogicNot < Trebuchet::Strategy::LogicBase\n\n def lau"
},
{
"path": "lib/trebuchet/strategy/logic_or.rb",
"chars": 256,
"preview": "require 'trebuchet/strategy/logic_base'\n\nclass Trebuchet::Strategy::LogicOr < Trebuchet::Strategy::LogicBase\n\n def laun"
},
{
"path": "lib/trebuchet/strategy/multiple.rb",
"chars": 951,
"preview": "class Trebuchet::Strategy::Multiple < Trebuchet::Strategy::Base\n\n attr_reader :strategies\n\n def initialize(args)\n @"
},
{
"path": "lib/trebuchet/strategy/nobody.rb",
"chars": 374,
"preview": "require 'singleton'\n# Nobody is to launch the feature to nobody\nclass Trebuchet::Strategy::Nobody < Trebuchet::Strategy:"
},
{
"path": "lib/trebuchet/strategy/per_denomination.rb",
"chars": 570,
"preview": "class Trebuchet::Strategy::PerDenomination < Trebuchet::Strategy::Base\n include Trebuchet::Strategy::PerDenominationabl"
},
{
"path": "lib/trebuchet/strategy/percent.rb",
"chars": 364,
"preview": "class Trebuchet::Strategy::Percent < Trebuchet::Strategy::Base\n\n include Trebuchet::Strategy::Percentable\n\n def initia"
},
{
"path": "lib/trebuchet/strategy/percent_deprecated.rb",
"chars": 385,
"preview": "class Trebuchet::Strategy::PercentDeprecated < Trebuchet::Strategy::Base\n\n include Trebuchet::Strategy::PercentableDepr"
},
{
"path": "lib/trebuchet/strategy/stub.rb",
"chars": 411,
"preview": "class Trebuchet\n module Strategy\n\n class Stub < Trebuchet::Strategy::Base\n attr_reader :state\n\n def initia"
},
{
"path": "lib/trebuchet/strategy/user_id.rb",
"chars": 410,
"preview": "class Trebuchet::Strategy::UserId < Trebuchet::Strategy::Base\n\n attr_reader :user_ids\n\n def initialize(user_ids)\n u"
},
{
"path": "lib/trebuchet/strategy/visitor_experiment.rb",
"chars": 556,
"preview": "class Trebuchet::Strategy::VisitorExperiment < Trebuchet::Strategy::Base\n\n include Trebuchet::Strategy::Experimentable\n"
},
{
"path": "lib/trebuchet/strategy/visitor_percent.rb",
"chars": 546,
"preview": "class Trebuchet::Strategy::VisitorPercent < Trebuchet::Strategy::Base\n\n include Trebuchet::Strategy::Percentable\n\n def"
},
{
"path": "lib/trebuchet/strategy/visitor_percent_deprecated.rb",
"chars": 567,
"preview": "class Trebuchet::Strategy::VisitorPercentDeprecated < Trebuchet::Strategy::Base\n\n include Trebuchet::Strategy::Percenta"
},
{
"path": "lib/trebuchet/strategy.rb",
"chars": 7777,
"preview": "require 'digest/sha1'\n\nmodule Trebuchet::Strategy\n\n def self.for_feature(feature)\n stub_state = Trebuchet::Feature.s"
},
{
"path": "lib/trebuchet/version.rb",
"chars": 50,
"preview": "class Trebuchet\n\n VERSION = \"0.12.1\".freeze\n\nend\n"
},
{
"path": "lib/trebuchet.rb",
"chars": 5905,
"preview": "require 'digest/sha1'\nrequire 'forwardable'\nclass Trebuchet\n # initialize a single one to save object allocations\n # T"
},
{
"path": "lib/trebuchet_rails/engine.rb",
"chars": 247,
"preview": "require 'rails'\nrequire File.expand_path(File.dirname(__FILE__) + \"/../trebuchet\")\n\n\nmodule TrebuchetRails\n class Engin"
},
{
"path": "lib/trebuchet_rails.rb",
"chars": 53,
"preview": "require 'trebuchet_rails/engine'\n\nrequire 'trebuchet'"
},
{
"path": "spec/custom_request_aware_strategy_spec.rb",
"chars": 1446,
"preview": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::CustomRequestAware do\n it \"should launch according to the custom s"
},
{
"path": "spec/custom_strategy_spec.rb",
"chars": 1811,
"preview": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::Custom do\n\n it \"should launch according to the custom strategy\" do"
},
{
"path": "spec/default_strategy_spec.rb",
"chars": 746,
"preview": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::Default do\n\n it \"should not launch if no strategy was defined\" do\n"
},
{
"path": "spec/disabled_backend_spec.rb",
"chars": 653,
"preview": "require 'spec_helper'\n\ndescribe Trebuchet::Backend::Disabled do\n \n before do\n Trebuchet.backend = :disabled\n Tre"
},
{
"path": "spec/everyone_strategy_spec.rb",
"chars": 660,
"preview": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::Everyone do\n\n it \"should be named everyone\" do\n Trebuchet::Stra"
},
{
"path": "spec/experiment_strategy_spec.rb",
"chars": 4704,
"preview": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::Experiment do\n \n before do\n @feature_name = \"Photographic Memo"
},
{
"path": "spec/feature_spec.rb",
"chars": 4917,
"preview": "require 'spec_helper'\n\ndescribe Trebuchet::Feature do\n \n def feature\n Trebuchet.feature('some_feature')\n end\n\n de"
},
{
"path": "spec/logic_and_strategy_spec.rb",
"chars": 1304,
"preview": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::LogicAnd do\n\n it \"passes the check only when all conditions are me"
},
{
"path": "spec/logic_base_strategy_spec.rb",
"chars": 921,
"preview": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::LogicBase do\n\n it \"should set @feature on sub-strategies\" do\n f"
},
{
"path": "spec/logic_not_strategy_spec.rb",
"chars": 383,
"preview": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::LogicNot do\n\n it \"works expectedly as a not operator\" do\n Trebu"
},
{
"path": "spec/logic_or_strategy_spec.rb",
"chars": 1295,
"preview": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::LogicOr do\n\n it \"passes the check as long as one of the children s"
},
{
"path": "spec/multiple_strategy_spec.rb",
"chars": 1883,
"preview": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::Multiple do\n\n it \"should support chaining strategies\" do\n Trebu"
},
{
"path": "spec/nobody_strategy_spec.rb",
"chars": 648,
"preview": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::Nobody do\n\n it \"should be named nobody\" do\n Trebuchet::Strategy"
},
{
"path": "spec/per_denomination_strategy_spec.rb",
"chars": 4251,
"preview": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::PerDenomination do\n describe 'launch' do\n before(:each) do\n "
},
{
"path": "spec/percent_deprecated_strategy_spec.rb",
"chars": 4946,
"preview": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::PercentDeprecated do\n\n it \"should be deprecated\" do\n Trebuchet:"
},
{
"path": "spec/percent_strategy_spec.rb",
"chars": 3001,
"preview": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::Percent do\n\n it \"should not launch to unsaved users, users with no"
},
{
"path": "spec/redis_backend_spec.rb",
"chars": 1370,
"preview": "require 'spec_helper'\nrequire 'mock_redis'\nrequire 'trebuchet/backend/redis'\n\n\ndescribe Trebuchet::Backend::Redis do\n\n "
},
{
"path": "spec/redis_hammerspaced_spec.rb",
"chars": 4074,
"preview": "require 'spec_helper'\nrequire 'mock_redis'\nrequire 'trebuchet/backend/redis_hammerspaced'\n\n\ndescribe Trebuchet::Backend:"
},
{
"path": "spec/spec_helper.rb",
"chars": 1063,
"preview": "$:.unshift File.dirname(__FILE__) + '/../lib'\n\nrequire 'bundler'\n\n# Bundler breaks things\nBundler.require :default, :tes"
},
{
"path": "spec/stubbing_spec.rb",
"chars": 2236,
"preview": "require 'spec_helper'\n\ndescribe Trebuchet::Feature::Stubbing do\n\n before do\n Trebuchet.dismantle_stubs\n end\n\n desc"
},
{
"path": "spec/trebuchet_spec.rb",
"chars": 5779,
"preview": "require 'spec_helper'\n\ndescribe Trebuchet do\n\n describe \"launch?\" do\n\n it \"should call launch_at? on feature\" do\n "
},
{
"path": "spec/user.rb",
"chars": 92,
"preview": "class User < Struct.new(:id, :role)\n\n def has_role?(role)\n self.role == role\n end\n\nend\n"
},
{
"path": "spec/user_id_strategy_spec.rb",
"chars": 1250,
"preview": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::UserId do\n\n it \"should only launch to designated users\" do\n Tre"
},
{
"path": "spec/visitor_experiment_strategy_spec.rb",
"chars": 1457,
"preview": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::VisitorExperiment do\n \n before do\n @feature_name = \"Infrared V"
},
{
"path": "spec/visitor_percent_deprecated_strategy_spec.rb",
"chars": 4484,
"preview": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::VisitorPercentDeprecated do\n\n it \"should be deprecated\" do\n Tre"
},
{
"path": "spec/visitor_percent_strategy_spec.rb",
"chars": 2497,
"preview": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::VisitorPercent do\n\n it \"should not break if no visitor id is set\" "
},
{
"path": "trebuchet.gemspec",
"chars": 957,
"preview": "# -*- encoding: utf-8 -*-\n$:.push File.expand_path(\"../lib\", __FILE__)\nrequire 'trebuchet'\n\nGem::Specification.new do |s"
}
]
About this extraction
This page contains the full source code of the airbnb/trebuchet GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 81 files (112.8 KB), approximately 33.7k tokens, and a symbol index with 309 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.