Bullet Train Usage Limits
Bullet Train provides a holistic method for defining model-based usage limits in your Rails application.
Installation
1. Purchase Bullet Train Pro
First, purchase Bullet Train Pro. Once you've completed this process, you'll be issued a private token for the Bullet Train Pro package server. The process is currently completed manually, so you may have to wait a little to receive your keys.
2. Install the Package
2.1. Add the Private Ruby Gems
You'll need to specify both Ruby gems in your Gemfile
, since we have to specify a private source for both:
source "https://YOUR_TOKEN_HERE@gem.fury.io/bullettrain" do
gem "bullet_train-billing"
gem "bullet_train-billing-stripe" # Or whichever billing provider you're using.
gem "bullet_train-billing-usage"
end
2.2. Bundle Install
bundle install
2.3. Copy Database Migrations
Use the following two commands on your shell to copy the required migrations into your local project:
cp `bundle show --paths | grep bullet_train-billing | sort | head -n 1`/db/migrate/* db/migrate
cp `bundle show --paths | grep bullet_train-billing-usage | sort | head -n 1`/db/migrate/* db/migrate
Note this is different than how many Rails engines ask you to install migrations. This is intentional, as we want to maintain the original timestamps associated with these migrations.
2.4. Run Migrations
rake db:migrate
2.5. Add Model Support
There are two concerns that need to be added to your application models.
The first concern is Billing::UsageSupport
and it allows tracking of usage of verbs on the models you want to support tracking usage on. It is recommended to add this capability to all models, so you can add this.
# app/models/application_record.rb
class ApplicationRecord
include Billing::UsageSupport
end
The second concern is Billing::Usage::HasTrackers
and it allows any model to hold the usage tracking. This is usually done on the Team
model.
# app/models/team.rb
class Team
include Billing::Usage::HasTrackers
end
Configuration
Usage limit configuration piggybacks on your product definitions in config/models/billing/products.yml
. It may help to make reference to the default product definitions in the Bullet Train starter repository.
Basic Usage Limits
All limit definitions are organized by product, then by model, and finally by verb. For example, you can define the number of projects a team is allowed to have on a basic plan like so:
basic:
prices:
# ...
limits:
projects:
have:
count: 3
enforcement: hard
upgradable: true
Any verb that your model supports can be used. It is recommended to standardize on using the action as the verb. For example, when creating a new model, use the verb create. When deleting a model, use the verb delete, and so on.
It's important to note that have
is a special verb and represents the simple count
of a given model on a Team
. All other verbs will be interpreted as time-based usage limits.
Options
enforcement
can behard
orsoft
.- When a
hard
limit is hit, the create form will be disabled. - When a
soft
limit is hit, users are simply notified, but can continue to surpass the limit.
- When a
upgradable
indicates whether or not a user should be prompted to upgrade when they hit this limit.
have
Usage Limits
Excluding Records from All models have an overridable billable
scope that includes all records by default. You can override this scope on any given model to ensure that certain records are filtered out from consideration when calculating usage limits. For example, we do the following on Membership
to exclude removed team members from contributing to any limitation put on the number of team members, like so:
scope :billable, -> { current_and_invited }
Another example may be excluding "archived" items within the billable usage count such as:
module Archivable
extend ActiveSupport::Concern
included do
scope :billable, -> { where(archived_at: nil) }
# ...
end
end
Time-Based Usage Limits
Configuring Limits
In addition to simple have
usage limits, you can specify other types of usage limits by defining other verbs. For example, you can limit the number of blog posts that can be published in a 3-day period on the free plan like this:
free:
limits:
blogs/posts:
publish:
count: 1
duration: 3
interval: days
enforcement: hard
count
is how many times something can happen.duration
andinterval
represent the time period we'll track for, e.g. "3 days" in this case.- Valid options for
interval
are anything you can append to an integer, e.g.minutes
,hours
,days
,weeks
,months
, etc., both plural and singular.
Tracking Usage
For these custom verbs, it's important to also instrument the application for tracking when these actions have taken place. For example:
class Blogs::Post < ApplicationRecord
# ...
def publish!
update(published_at: Time.zone.now)
track_billing_usage(:published)
end
end
It is important that you use the past tense of the verb when tracking so it is tracked appropriately. If you'd like to increment the usage count by more than one, you can pass a quantity like count: 5
to this call.
Cycling Trackers Regularly
We include a Rake task you'll need to run on a regular basis in order to cycle the usage trackers that are created at a Team
level. By default, you should probably run this every five minutes:
rake billing:cycle_usage_trackers
Checking Usage Limits
Checking Time-Based Limits
To make decisions based on or enforce time-based hard
limits in your views and controllers, you can use the current_limits
helper like this:
current_limits.can?(:publish, Blogs::Post)
(You can also pass quantities like count: 5
as an option.)
Checking Basic Limits
For basic have
limits, forms generated by Super Scaffolding will be automatically disabled when a hard
limit has been hit. Index views will also alert users to a limit being hit or broken for both hard
and soft
limits.
If your Bullet Train application scaffolding predates this feature, you can reference the newest Tangible Things index template and form template to see how we're using the
shared/limits/index
andshared/limits/form
partials to present and enforce usage limits, and copy this usage in your own views.
To make decisions in other views or within controllers, you can use the #exhausted?
method on the current_limits
helper to check if any basic have
limits have been hit or exceeded.
# Inspects broken hard limits by default
current_limits.exhausted?(Blogs::Post)
# Or you can specify the `enforcement` level
current_limits.exhausted?(Blogs::Post, :soft)
Presenting an Error
If you want to present an error or warning to the user based on their usage, there themed alert partials that can be displayed in your views. These partials can be rendered via the path shared/limits/error
and shared/limits/warning
respectively.
<%= render "shared/limits/warning", model: model.class %>
<%= render "shared/limits/error", action: :create, model: model.class, count: 1, cancel_path: cancel_path %>
These partials have various local assigns that are used to configure the partial.
action
- theverb
to check the usage on the model for. Defaults tohave
.color
- thecolor
value to use for the alert partial. Defaults tored
for errors andyellow
for warnings.count
- the number of objects intended to be acted upon. Defaults to 1.model
- theclass
relationship as the model to inspect usage on. Defaults toform.object.class
.
Changing Products
The default list of products that the limits are based off of are the products available from your Billing products, which is most likely a list of subscription plans from Stripe.
You can change this default behavior by creating your own billing limiter and overriding the current_products
method. This method should return a list of products that all respond to a limits
message. The rest of the behavior is encapsulated in the Billing::Limiter::Base
concern.
# app/models/billing/email_limiter.rb
class Billing::EmailLimiter
include Billing::Limiter::Base
def current_products
products = Billing::Product.data
EmailService.retrieve_product_tiers.map do |tier|
add_product_limit(tier, products)
end
end
end