-
Notifications
You must be signed in to change notification settings - Fork 14
How to track if user is active
written by @mareczek
Hi! So, Hyperloop is very connected - the backend data models connect to the UI automagically. The data arrives by just writing User.find(123).name
, and it gets updated automagically as well.
But, how can we tell if a User is currently signed in or not? We have not found any built in method that would have an answer for this question. Hence, this short how to (our specific scenario).
This is not a offical howto :-) It's just how we (EngineArch) implemented this in a project where we use Hyperloop (which we love). We are very open for suggestions!
We decided to cache this information on the User model (in the db), hence a migration file. We would like to know:
- since when is the user active (signed in)
- or since when is the user inactive
Having a User
, we add two datetime attributes:
bundle exec rails generate migration add_session_timestamps_to_user active_since:datetime inactive_since:datetime
you should now have a migration file generated in <app root>/db/migrations/<timestamp>_add_session_timestamps_to_user.rb
with the contents:
class AddSessionTimestampsToUser < ActiveRecord::Migration[5.1]
def change
add_column :users, :active_since, :datetime
add_column :users, :inactive_since, :datetime
end
end
We do not intend on searching the DB based on these attributes, hence no index
applied. If in your use-case you will be filtering the DB based on these attributes do not hesitate to add a index
.
Run the migration:
bundle exec rails db migrate
In this step we will hook into the lifecycle of Hyperloop::Connection
which is an ActiveRecord
object, with standard Rails callbacks. We rely on Hyperloop to manage these entities.
Navigate to <app root>/config/initializers
and create a file called hyperloop_connection.rb
.
Copy-paste the below content and save the file:
module Hyperloop
class Connection < ActiveRecord::Base
unless RUBY_ENGINE == 'opal'
after_create :log_active_since_on_user, if: :user_specific_channel?
after_destroy :log_inactive_since_on_user, if: :user_specific_channel?
def user_specific_channel?
channel =~ /User-[0-9]+/
end
def user
channel =~ /User-([0-9]+)/ ? @user ||= User.find_by_id($1) : nil
end
def log_active_since_on_user
puts "Hyperloop::Connection: WILL log_active_since_on_user, #{user.try(:id)}"
user.try(:update_attributes, { active_since: Time.now, inactive_since: nil })
end
def log_inactive_since_on_user
puts "Hyperloop::Connection: WILL log_inactive_since_on_user, #{user.try(:id)}"
user.try(:update_attributes, { active_since: nil, inactive_since: Time.now })
end
end
end
end
The above code will run only on the server side of you application thanks to
unless RUBY_ENGINE == 'opal'
Note: the lines with
puts
are optional - we use them for debugging
Whenever your app server is down all of the connections within you app are obviously lost. Hence, whenever the server (production, staging or any other env) starts it is a good idea to reset all active_since
. We will not touch the inactive_since
attribute since we will never be certain when the server was turned off or crashed.
Navigate to <app root>/config/initializers
and create a file called user_session_timestamp_reset.rb
.
Copy-paste the blow content and save the file.
if Hyperloop.on_server?
user_model = begin
Object.const_get 'User'
rescue LoadError
rescue NameError => e
puts "Model `User` does not exist, hence will not reset session timestamps"
end
if user_model
if (missing_columns = ['active_since', 'inactive_since'] - User.column_names).empty?
result = User.update_all(active_since: nil)
puts "Updated all User records with `active_since: nil` -> #{result}"
else
puts "`User` table does not have columns: #{missing_columns.join(', ')}. ...add them :)"
end
end
end
End.
If everything went smooth you are now able to see the timestamps on you User
instances.
We use these attributes to represent the session state of a user with a Hyperloop::Component
:
class UserStatus < Hyperloop::Component
param active_since: nil
param inactive_since: nil
def render
div(class: "person-status #{calculate_status}")
end
def calculate_status
if params.active_since
'online'
elsif params.inactive_since && params.inactive_since > (Time.now - 30.minutes)
'away'
else
'offline'
end
end
end