Spectator Sport
Record and replay browser sessions in a self-hosted Rails Engine.
Spectator Sport uses the rrweb library to create recordings of your website's DOM as your users interact with it. These recordings are stored in your database for replay by developers and administrators to analyze user behavior, reproduce bugs, and make building for the web more fun.
🚧 🚧 This gem is very early in its development lifecycle and will undergo significant changes on its journey to v1.0. I would love your feedback and help in co-developing it, just fyi that it's going to be so much better than it is right now.
🚧 🚧 Future Roadmap:
- ✅ Proof of concept and technical demo
- ✅ Running in production on Ben Sheldon's personal business websites
- ✅ Publish manifesto of principles and intent
- ◻️ Reliable and efficient event stream transport
- ✅ Player dashboard design using Bootstrap and Turbo (#20)
- ◻️ Automatic cleanup of old recordings to minimize database space
- ◻️ Identity methods for linking application users to recordings
- ◻️ Privacy controls with masked recording by default
- ◻️ Automated installation process with Rails generators
- ◻️ Fully documented installation process
- 🏁 Release v1.0 🎉
- ◻️ Live streaming replay of recordings
- ◻️ Searching / filtering of recordings, including navigation and 404s/500s, button clicks, rage clicks, dead clicks, etc.
- ◻️ Custom events
- 💖 Your feedback and ideas. Please open an Issue or Discussion or even a PR modifying this Roadmap. I'd love to chat!
Installation
The Spectator Sport gem is conceptually two parts packaged together in this single gem and mounted in your application:
- The Recorder, including javascript that runs in the client browser and produces a stream of events, an API endpoint to receive those events, and database migrations and models to store the events as a cohesive recording.
- The Player Dashboard, an administrative dashboard to view and replay stored recordings
To install Spectator Sport in your Rails application:
- Add
spectator_sportto your application's Gemfile and install the gem:bundle add spectator_sport
- Install Spectator Sport in your application. 🚧 This will change on the path to v1. Explore the
/demoapp as live example:-
Create database migrations with
bin/rails g spectator_sport:install:migrations. Apply migrations withbin/rails db:prepare -
Mount the recorder API in your application's routes with
mount SpectatorSport::Engine, at: "/spectator_sport, as: :spectator_sport" -
Add the
spectator_sport_script_tagshelper to the bottom of the<head>oflayout/application.rb. Example:<%# app/views/layouts/application.html.erb %> <%# ... %> <%= spectator_sport_script_tags %> </head>
-
Add a
<script>tag topublic/404.html,public/422.html, andpublic/500/htmlerror pages. Example:<!-- public/404.html --> <!-- ... --> <script defer src="/spectator_sport/events.js"></script> </head>
-
- To view recordings, you will want to mount the Player Dashboard in your application and set up authorization to limit access. See the section on Dashboard authorization for instructions.
Tagging recordings
You can associate a string tag (e.g. a user ID or account identifier) with the current recording by calling spectator_sport_tag_recording in any template:
<%= spectator_sport_tag_recording(current_user.id.to_s) %>This renders a hidden <meta> element signed by the server. The recording client detects it and immediately sends it to the API, where the signature is verified before the tag is stored. Tags are displayed in the dashboard and can be used to look up all recordings associated with a given value.
Note: this requires the spectator_sport_session_window_tags migration to be applied (bin/rails spectator_sport:install:migrations && bin/rails db:migrate). If the migration hasn't been run, the feature is silently disabled.
Labeling recordings
You can associate key-value labels with the current recording by calling spectator_sport_label_recording in any template:
<%= spectator_sport_label_recording(current_user.id.to_s, key: "user_id", strategy: :one) %>
<%= spectator_sport_label_recording("admin", key: "role", strategy: :many) %>
<%= spectator_sport_label_recording("vip") %>The key argument is optional. When omitted, the label behaves like a tag. The strategy argument (default :many) controls how values are stored per recording:
-
strategy: :many(default): multiple values for the same key are accumulated per recording. -
strategy: :one: only one value per key is kept per recording. A later call with the same key replaces the stored value. Without a key, behaves like:many. -
strategy: :first: only the first value for a key is stored; subsequent calls with the same key are ignored. Requires a key.
This renders a hidden <meta> element signed by the server. The recording client detects it and immediately sends it to the API, where the signature is verified before the label is stored. Labels are displayed in the dashboard and can be used to look up all recordings associated with a given key-value pair.
Note: this requires the spectator_sport_labels migration to be applied (bin/rails spectator_sport:install:migrations && bin/rails db:migrate). If the migration hasn't been run, the feature is silently disabled.
Stopping recording
You can pause recording for a page by calling spectator_sport_stop_recording in any template:
<%= spectator_sport_stop_recording %>This renders a hidden <meta> element that the recording client detects on page load. When present, rrweb recording is stopped and no events are buffered or sent. Recording automatically resumes when the user navigates to a page that does not have this tag.
This is useful when navigating via Turbo to pages that shouldn't be recorded — without this tag the recorder would continue running across navigations.
Dashboard authorization
It is advisable to manually install and set up authorization for the Player Dashboard and refrain from making it public.
If you are using Devise, the process of authorizing admins might resemble the following:
# config/routes.rb
authenticate :user, ->(user) { user.admin? } do
mount SpectatorSport::Dashboard::Engine, at: 'spectator_sport_dashboard', as: :spectator_sport_dashboard
endOr set up Basic Auth:
# config/initializers/spectator_sport.rb
SpectatorSport::Dashboard::Engine.middleware.use(Rack::Auth::Basic) do |username, password|
ActiveSupport::SecurityUtils.secure_compare(Rails.application.credentials.spectator_sport_username, username) &
ActiveSupport::SecurityUtils.secure_compare(Rails.application.credentials.spectator_sport_password, password)
endIf you are using an authentication method similar to the one used in ONCE products, you can utilize an auth constraint in your routes.
# config/routes.rb
class AuthRouteConstraint
def matches?(request)
return false unless request.session[:user_id]
user = User.find(request.session[:user_id])
if user && user.admin?
cookies = ActionDispatch::Cookies::CookieJar.build(request, request.cookies)
token = cookies.signed[:session_token]
return user.sessions.find_by(token: token)
end
end
end
Rails.application.routes.draw do
# ...
namespace :admin, constraints: AuthRouteConstraint.new do
mount SpectatorSport::Dashboard::Engine, at: 'spectator_sport_dashboard', as: :spectator_sport_dashboard
end
endOr extend the SpectatorSport::Dashboard::ApplicationController with your own authorization logic:
# config/initializers/spectator_sport.rb
ActiveSupport.on_load(:spectator_sport_dashboard_application_controller) do
# context here is SpectatorSport::Dashboard::ApplicationController
before_action do
raise ActionController::RoutingError.new('Not Found') unless current_user&.admin?
end
def current_user
# load current user from session, cookies, etc.
end
endContributing
💖 Please don't be shy about opening an issue or half-baked PR. Your ideas and suggestions are more important to discuss than a polished/complete code change.
This repository is intended to be simple and easy to run locally with a fully-featured demo application for immediately seeing the results of your proposed changes:
# 1. Clone this repository via git
# 2. Set it up locally
bundle install
# 3. Create database
bin/rails db:setup
# 4. Run the demo Rails application:
bin/rails s
# 5. Load the demo application in your browser
open http://localhost:3000
# 6. Make changes, see the result, commit and make a PR!Releasing a new version
- Update the version in
lib/spectator_sport/version.rb - Run
bundle installto updateGemfile.lock - Commit the version bump and updated
Gemfile.lock:git add lib/spectator_sport/version.rb Gemfile.lock git commit -m "Bump version to x.y.z" - Build and publish to RubyGems, tag, and push to GitHub:
bundle exec rake release - Create a GitHub Release at https://github.com/bensheldon/spectator_sport/releases using the new
vx.y.ztag.
License
The gem is available as open source under the terms of the MIT License.