Client and Server Feature Flipping
The Who & Elton John - Pinball Wizard (Tommy 1975)
PinballWizard
brings feature flipping into a simple and uniform API in both client-side JavaScript and Ruby.
Why?
PinballWizard is intended to work with heavily cached pages (e.g. Varnish) that need feature flipping. It works well with third parties such as Optimizely where flipping occurs after HTML is rendered.
What is a feature?
A set of Ruby, HTML, JavaScript, and CSS that can be turned on or off.
A feature is simply a name and default state:
- active: If it is currently turned on and running.
- inactive: Not turned on.
- disabled: Not turned on and cannot be activated.
Building
- Define and register the feature in the Ruby app.
- Build the JavaScript component.
- Write the corresponding HTML and CSS
- Activate and test your feature.
Ruby
Define your feature (typically in config/initializers/pinball_wizard.rb
).
PinballWizard::DSL.build do
# Active when the page loads:
feature :example, active: true
# Deactive when the page loads:
feature :example, active: false
end
You can also pass in a proc for situtations where active/inactive is conditional. Returning false will make the feature inactive.
PinballWizard::DSL.build do
feature :example, active: proc { }
end
HTML
Once the feature is registered, you can use Slim to include the HTML partial found at app/views/features/example.slim
. This is only included if the feature is available, which allows the HTML to stay small.
= feature 'example'
To use a different partial for the same feature, pass in the partial:
key. This will use app/views/features/example_button.slim
.
If the feature is not active immediately, it's recommended to hide the HTML with inline or external CSS.
= feature 'example', partial: :example_button'
CSS
PinballWizard automatically adds and removes CSS classes named .use-{feature-name}
and without-{feature-name}
to the <html>
tag.
This supports keeping the global and feature-flipped CSS styles separated.
It is recommended to organize your CSS like so:
// app/assets/stylesheets/features/_example.scss
.use-example {
// CSS when the 'example' feature is active.
}
Then include it on the main file with @import 'features/example'
In your main SCSS file, wrap all CSS rules for the inactive state in the without-{feature-name}
class, like so:
.without-example {
// CSS when the 'example' feature is inactive;
// i.e. current production code.
}
When your feature is published (made part of the permanent codebase), you can simply delete the entire .without-example
section and remove the .use-example
class wrapper.
When using .use-{feature-name}
, you may notice a shift or flicker in the UI. This occurs when pinball's JavaScript executes after the DOMContentReady
event. To prevent this, add dist/css_tagger.min.js
into your <head>
tag. For example:
<head>
<script type="text/javascript">
// paste snippet from dist/css_tagger.min.js
</script>
</head>
be certain to update your usage of this snippet when updating pinball_wizard
JavaScript
Features subscribe to events and respond when they're activated or deactivated. It no longer needs to know about Optimizely, cookies, or url params. (Single Responsibility Principle FTW)
One advantage to this approach is that you can activate features after the DOM is loaded (for testing).
When pinball runs, it will automatically activate the features.
Example AMD/RequireJS Module
define ['pinball_wizard'], (pinball) ->
# Define feature component functions here ...
pinball.subscribe 'example',
->
# callback when activated. e.g. show it
->
# callback when deactivated. e.g. hide it.
# Return component functions to expose.
Example Flight Component
define ['pinball_wizard'], (pinball) ->
@after 'initialize', ->
pinball.subscribe 'example',
->
# callback when activated. e.g. show it
->
# callback when deactivated. e.g. hide it.
Asking (pseudo-code)
As an alternative, a feature may check if it's active. This method is not preferred since it only occurs once during page load.
define ['pinball_wizard'], (pinball) ->
if pinball.isActive('example')
# Do something
Activating Features for Testing (once)
With a URL Param
Add pinball
to the URL (e.g. ?pinball=example_a,example_b
).
Post-Render (after page load)
pinball.activate('example');
pinball.deactivate('example');
Activating a feature that is already active or disabled will have no effect.
Optionally Supply a Source
Add an optional source as a second argument to help know where features are activated while debugging.
pinball.activate('example','source name');
Activating Features (permanently)
To turn on and keep a feature on, you can activate it permanently in the console. This is only for your browser's session.
pinball.activatePermanently('example')
You can permanently activate multiple features like so:
pinball.activatePermanently('example1', 'example2')
See a List of Permanent Features
pinball.permanent()
Reset Permanent Features
pinball.resetPermanent()
JsConfig Registry
The application keeps a list of features and passes them in the JsConfig object (e.g. window.pinball
). These define what's available and activated on page load.
Plain JavaScript
<head>
<script type="text/javascript">
window.pinball = window.pinball || []
window.pinball.push(['add', { "feature_a": "active", "feature_b": "inactive", "feature_c": "disabled" }]);
</script>
<head>
Ruby (example is in slim)
head
javascript:
window.pinball = window.pinball || [];
window.pinball.push(['add', #{{PinballWizard::Registry.to_h.to_json}}]);
Debugging
Add it to the url:
?pinball=debug
Turn on logging in JavaScript:
pinball.debug();
Show current state in the JavaScript console:
pinball.state();
Integrations
For RentPath specific functionality, including ConFusion, see pinball_wizard-rentpath
Extra: Custom Ruby Class
By default, features are instances of PinballWizard::Feature
. You can define your own class and register it according to a hash key. This is useful to disable features.
# e.g. app/features/my_feature.rb
module PinballWizard
class MyFeature < Feature
def determine_state
if my_condition?
disable "My Feature: My Reason"
end
end
end
end
# e.g. config/initializers/pinball_wizard.rb
PinballWizard.configure do |c|
c.class_patterns = my_option: PinballWizard::MyFeature
end
# e.g. config/features.rb
PinballWizard::DSL.build do
feature :example, :my_option
end
Getting started for development.
git checkout dev
git pull origin dev
git checkout -b my_branch
npm run preversion
A basic red-green-refactor workflow.
npm install
npm run watch
npm run watch:test # in another terminal window or pane
Examples of common tasks.
npm script commands are defined in the scripts section of package.json. To see a full list of available npm commands, run:
npm run
Install NPM Dependencies
npm install
One-time compile of application source and tests.
npm run compile
Compile application source & tests and build the distribution as files change.
npm run watch
Running Tests.
One time.
npm test
Watch continuously and run tests when code or specs change.
npm run watch:test
Cleaning
Remove compiled code and tests in .tmp/.
npm run clean
Remove compiled code and tests, node_modules
npm run clean:all
Remove compiled code, tests, node_modules
; reinstall
node modules; recompile code and tests.
npm run reset
Building a Distribution
To build a distribution and tag it, run one of the following commands.
npm version patch -m "Bumped to %s"
npm version minor -m "Bumped to %s"
npm version major -m "Bumped to %s"
There's a 'preversion' script in package.json that does the following:
- Remove the .tmp/ directory.
- Remove the node_modules directories.
- Install all npm packages.
- Compile the application source and specs.
- Run the tests.
- Rebuild the distribution.
Just build a distribution.
npm run build
Notes
- The
dist/
directory must be part of the repo - don't gitignore it!
Contributing
Fork and submit a pull request. This is a README driven development process, please contribute by modifying this document.
Credits
- Pinball photo: