NOTE: this has been updated in the #2 to use the newest version of the Prefab Ruby SDK
This repo shows how to add Prefab to your Rails app to get access to features like
- Dynamic log levels
- Feature flags
- Live config
Follow along on YouTube
We start with rails new example-app --css tailwind
We can run the app from the app root with bin/dev
and see the default Rails content at http://localhost:3000
Adding Prefab to our new Rails app is easy.
Add gem "prefab-cloud-ruby"
to your Gemfile
and bundle install
In config/application.rb
, we set up the client as a global for easy access throughout our app.
We also hook into the Rails loggers so we can use Prefab for dynamic log levels.
$prefab = Prefab::Client.new
$prefab.set_rails_loggers
Because Rails uses Puma out-of-the-box with process forking, we'll also need to do the same in our on_worker_boot
in config/puma.rb
1
on_worker_boot do
$prefab = Prefab::Client.new
$prefab.set_rails_loggers
end
The last part of adding Prefab to your Rails app is to get an API key from https://app.prefab.cloud and set it as the environment variable PREFAB_API_KEY
. We'll restart our app to make sure it uses that env var.
That's all it takes to add Prefab to your app. Now let's take some of the features for a test drive.
The first feature to demo is Dynamic log levels. To get a feel for this feature, we'll add a HomeController
with a simple view and map the home controller to our root path.
In app/controllers/home_controller.rb
, we add example logging at three different levels
class HomeController < ApplicationController
def index
logger.debug '🔍 Hello from Rails.logger.debug'
logger.info 'ℹ️ Hello from Rails.logger.info'
logger.warn '⚠️ Hello from Rails.logger.warn'
logger.error '🚨 Hello from Rails.logger.error'
end
end
By default, the Prefab client logs at the warn
level. When we visit http://localhost:3000 we should see the error
and warn
output but not the debug or info output.
In the Prefab UI, let's set our "Root Log Level" to "INFO". Now reloading http://localhost:3000 shows the error
, warn
, and info
output. Note that a number of items you're used to seeing in local Rails development are logged at the info
level.
Prefab lets you change log levels on the fly. We can even set log levels for specific models, controllers, methods, and actions.
If you liked dynamic log levels, you'll love targeted log levels.
Let's imagine we get a report of a bug that only happens in mobile. Ugh. We don't want to turn on more verbose logging for every request and blow up our log retention SaaS bill.
No problem. Prefab makes it easy to add rules to log levels to get just the logging you need exactly where you need it.
Let's set the "Root Log Level" to error. Refreshing http://localhost:3000 shows only the error
message.
Now let's expand our tree to zero-in on app.controllers.home_controller.index
and select Targeted
.
We'll set the "default value" to "Inherit" and click "Add Targeting Rule" -- this is where things get fun.
We'll set the level to DEBUG
when "Property is one of" for the property device.mobile
and set the value to true
.
Wait, how does Prefab know if the request comes from a mobile device? It doesn't yet. This is where Prefab's context concept comes in.
We'll add three methods in app/controllers/application_controller.rb
:
around_action do |_, block|
$prefab.with_context(prefab_context, &block)
end
def prefab_context
{
device: {
mobile: mobile?
}
}
end
def mobile?
request.user_agent&.match?(/(iPhone|iPod|iPad|Android)/)
end
We add an around_action
to give Prefab some knowledge of our request. So far the only context is whether or not the request is from a mobile device. The device.mobile
in our rule above maps to the context's device
node's mobile
attribute.
Reloading our desktop browser for http://localhost:3000 still only shows the error
output. But loading the same URL from the iPhone simulator shows debug
through error
output.
If this were a real-world bug, we'd have all the detail we could possibly get.
To the delight and terror of developers, most apps have users.
For this repo, we'll skip the boring user authentication and users bit and fake some example users. We'll use some avatars from https://userstock.io/ (except Jeff, our beloved CEO).
We'll add the user
to our context in app/controllers/application_controller.rb
def prefab_context
{
device: {
mobile: mobile?
},
user: {
id: current_user&.id,
email: current_user&.email,
country: current_user&.country,
}
}
end
Once you make Prefab aware of the current user for a request, you can target your log levels even further. Want to get debug-level logging for user with id 3 on their mobile device? Easy. For all users with an @example.com
email? Cake.
We can also start using Prefab's feature flags.
Right now http://localhost:3000/ shows the GDPR cookie consent banner to everyone. I'm not a lawyer but let's imagine our lawyer says that we don't need to show this banner to people in the US. Awesome, let's declutter our UI.
We'll wrap our cookie consent UI with a feature flag check.
<% if $prefab.enabled?("gdpr.banner") %>
...
<% end %>
By default an enabled?
check returns false
so we can't just ship this to production without setting up the flag first in the Prefab UI.
We'll add a new flag with the key gdpr-banner
of type bool
. We want it to return true
by default.
Next we click "Add Rule" and specify that we return false
when "Property is one of" for user.country
with value US
.
Now we save and publish our changes.
Our UI has three users. Tony is from the UK so if we "Sign in as Tony" we'll see the GDPR banner. Joan is from France so if we "Sign in as Joan" we still see the GDPR banner. But Jeff? He lives in the US so if you sign in as him, we'll see the GDPR banner is gone. Beautiful.
We're going to add one more feature to help us better understand how Prefab works in our app and to demonstrate Live Config.
First we add a new partial app/views/home/_prefab_values.html.erb
to render out the resolved Prefab values for our application. Imagine this showing up in your admin dashboard.
<div id="prefab-values" class="mt-24 space-y-4">
<p>Here's the resolved Prefab config for your application:</p>
<table class="border-separate w-full">
<thead class="bg-gray-50 text-left">
<!-- snip -->
</thead>
<tbody class="<%= $prefab.get("admin.prefab-values.classes", "") %>">
<% $prefab.resolver.presenter.each do |key, config| %>
<tr class="odd:bg-blue-50">
<td class="p-4 break-all"><%= key %></td>
<td class="p-4"><%= config.value %></td>
<td class="p-4"><%= $prefab.get(key) %></td>
<td class="p-4"><%= config.value.class %></td>
<td class="p-4"><%= config.source %></td>
</tr>
<% end %>
</tbody>
</table>
</div>
We reference this partial from the app/views/home/index.html.erb
view.
<%= render partial: "prefab_values" %>
Live config, feature flags, and log levels can all have different kinds of rules applied to them, so it can be helpful to see the default values for this request. Additionally, because we have the context of the user, we can show the "Evaluated Value" for the current user.
When we sign in as "Jeff" you'll note that the gdpr.banner
has a default value of true
but an evaluated value of false
.
When you change a value in Prefab, it pushes out the changes to your Rails app.2 We can use Turbo in Rails to see these updates in real-time.
First we'll register our turbo stream in app/views/home/index.html.erb
right above our partial rendering.
<%= turbo_stream_from(:prefab_values) %>
<%= render partial: "prefab_values" %>
Next, we'll edit our config/application.rb
to use this stream to push updates to our Prefab Values partial.
$prefab.on_update do
if defined?(Turbo::StreamsChannel)
Turbo::StreamsChannel.broadcast_replace_to :prefab_values,
target: 'prefab-values',
partial: 'home/prefab_values'
end
end
Since we made a change to config/application.rb
we'll need to restart our server and refresh the page.
Now for some fun. You might have noticed that we included a live config evaluation, $prefab.get("admin.prefab-values.classes", "")
, in our _prefab_values.html.erb
partial. The first argument to $prefab.get
is the config key and the second is the default.
Let's try it out! We'll add a config in the Prefab UI named admin.prefab-values.classes
. For the value, set font-mono
. Save and publish. Note that the table in our app updates immediately. Let's change the value to font-mono bg-yellow-50
and save and publish. Now our zebra stripes on the table alternate blue and yellow.
You've seen how easy it is to add Prefab to a Rails app. With dynamic log levels, feature flags, and live config, you can take control of your app's behavior like never before. Sign up today!
Footnotes
-
Why? Ruby uses a copy-on-write approach to forking. For Prefab, this means that a client initialized before forking is frozen-in-time and doesn't get access to streaming config updates. Because the loggers are also copied during the fork, we need to re-wrap them to allow for dynamic log levels. ↩
-
Via server-sent events. ↩