Skip to content

Commit

Permalink
Separate sessions from windows from events; add a more usable demo
Browse files Browse the repository at this point in the history
  • Loading branch information
bensheldon committed Sep 23, 2024
1 parent 4dab5d4 commit e7b974e
Show file tree
Hide file tree
Showing 18 changed files with 174 additions and 39 deletions.
14 changes: 12 additions & 2 deletions app/controllers/spectator_sport/dashboards_controller.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
module SpectatorSport
class DashboardsController < ApplicationController
def index
page_id = Event.last&.page_id
@events = Event.where(page_id: page_id).order(:created_at)
@session_windows = SessionWindow.order(:created_at).limit(50).reverse_order
end

def show
@session_window = SessionWindow.find(params[:id])
end

def destroy
@session_window = SessionWindow.find(params[:id])
@session_window.events.delete_all
@session_window.delete
redirect_to action: :index
end
end
end
18 changes: 11 additions & 7 deletions app/controllers/spectator_sport/events_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,22 @@ class EventsController < ApplicationController
skip_before_action :verify_authenticity_token

def create
data = if params.key?(:pageId) && params.key?(:events)
params.slice(:pageId, :events).stringify_keys
else
data = if params.key?(:sessionId)&& params.key?(:windowId) && params.key?(:events)
params.slice(:sessionId, :windowId, :events).stringify_keys
else
# beacon sends JSON in the request body
JSON.parse(request.body.read).slice("pageId", "events")
end
JSON.parse(request.body.read).slice("sessionId", "windowId", "events")
end

page_id = data["pageId"]
session_secure_id = data["sessionId"]
window_secure_id = data["windowId"]
events = data["events"]

session = Session.find_or_create_by(secure_id: session_secure_id)
window = SessionWindow.find_or_create_by(secure_id: window_secure_id, session: session)

records_data = events.map do |event|
{ page_id: page_id, event_data: event, created_at: Time.at(event["timestamp"] / 1000.0) }
{ session_id: session.id, session_window_id: window.id, event_data: event, created_at: Time.at(event["timestamp"] / 1000.0) }
end.to_a

Event.insert_all(records_data)
Expand Down
2 changes: 2 additions & 0 deletions app/models/spectator_sport/event.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
module SpectatorSport
class Event < ApplicationRecord
belongs_to :session
belongs_to :session_window
end
end
6 changes: 6 additions & 0 deletions app/models/spectator_sport/session.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module SpectatorSport
class Session < ApplicationRecord
has_many :session_windows
has_many :events
end
end
6 changes: 6 additions & 0 deletions app/models/spectator_sport/session_window.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module SpectatorSport
class SessionWindow < ApplicationRecord
belongs_to :session
has_many :events
end
end
22 changes: 13 additions & 9 deletions app/views/spectator_sport/dashboards/index.html.erb
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
<%= render "spectator_sport/shared/rrweb_player" %>
<h1>Spectator Sport</h1>
<% if @session_windows.to_a.any? %>
<ul>

<script type="module">
new rrwebPlayer({
target: document.body, // customizable root element
props: {
events: <%== @events.map(&:event_data).to_json %>,
},
});
</script>
<% @session_windows.each do |session_window| %>
<li>
<%= link_to "Session (##{session_window.session_id}) Window (##{session_window.id})", spectator_sport.dashboard_path(session_window.id) %>
<%= button_to "X", { action: :destroy, id: session_window.id }, method: :delete, form: { style: "display: inline-block" } %>
</li>
<% end %>
</ul>
<% else %>
<em>No recordings found.</em>
<% end %>
10 changes: 10 additions & 0 deletions app/views/spectator_sport/dashboards/show.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<%= render "spectator_sport/shared/rrweb_player" %>

<script type="module">
new rrwebPlayer({
target: document.body, // customizable root element
props: {
events: <%== @session_window.events.order(:created_at).map(&:event_data).to_json %>,
},
});
</script>
45 changes: 31 additions & 14 deletions app/views/spectator_sport/shared/_script_tags.erb
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,26 @@ or you can use record.mirror to access the mirror instance during recording.`;le
</script>

<script>
const URL = 'spectator_sport/events';
// generate string of 40 random characters
const pageId = [...Array(40)].map(() => Math.random().toString(36)[2]).join('');
const POST_URL = '/spectator_sport/events';
const POST_INTERVAL_SECONDS = 2;

function generateRandomId() {
return [...Array(40)].map(() => Math.random().toString(36)[2]).join('');
}

const SESSION_ID_STORAGE_NAME = "spectator_sport_session_id";
let sessionId = window.localStorage.getItem("spectator_sport_session_id");
if (!sessionId) {
sessionId = generateRandomId();
window.localStorage.setItem(SESSION_ID_STORAGE_NAME, sessionId);
}

const WINDOW_ID_STORAGE_NAME = "spectator_sport_window_id";
let windowId = window.sessionStorage.getItem(WINDOW_ID_STORAGE_NAME);
if (!windowId) {
windowId = generateRandomId();
window.sessionStorage.setItem(WINDOW_ID_STORAGE_NAME, windowId);
}

const events = [];

Expand All @@ -20,16 +37,15 @@ or you can use record.mirror to access the mirror instance during recording.`;le
},
});


function postData() {
if (events.length === 0) {
return
}

const body = JSON.stringify({pageId, events});
const body = JSON.stringify({sessionId, windowId, events});
events.length = 0;

fetch(URL, {
fetch(POST_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand All @@ -39,18 +55,19 @@ or you can use record.mirror to access the mirror instance during recording.`;le
});
}

setInterval(postData, 10 * 1000);
setInterval(postData, POST_INTERVAL_SECONDS * 1000);

document.addEventListener("visibilitychange", function logData() {
console.log("visibilitychange", document.visibilityState, events.length);
if (document.visibilityState === "hidden") {
if (events.length === 0) {
return
// Client has navigated away, so post the data
if (events.length > 0) {
postData();
}

const body = JSON.stringify({pageId, events});
events.length = 0;

navigator.sendBeacon(URL, body)
// TODO: note that we have unloaded so that if the state is recovered from bf-cache, we know to reset the recorder
}
else if (document.visibilityState === "visible") {
// TODO: Detect if this is because of a back/forward event, which may be cached, so need to restart the recorder
}
});
</script>
3 changes: 2 additions & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
SpectatorSport::Engine.routes.draw do
root to: "dashboards#index"
resource :events, only: [:create]
resource :events, only: [ :create ]
resources :dashboards, only: [ :show, :destroy ]
end
19 changes: 17 additions & 2 deletions db/migrate/20240923140845_create_spectator_sport_events.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
class CreateSpectatorSportEvents < ActiveRecord::Migration[7.2]
def change
create_table :spectator_sport_sessions do |t|
t.timestamps
t.string :secure_id, null: false

t.index [ :secure_id, :created_at ]
end

create_table :spectator_sport_session_windows do |t|
t.timestamps
t.references :session, null: false
t.string :secure_id, null: false
end

create_table :spectator_sport_events do |t|
t.timestamps
t.string :page_id, null: false
t.references :session, null: false
t.references :session_window, null: true
t.json :event_data, null: false # TODO: jsonb for postgres ???

t.index [:page_id, :created_at]
t.index [ :session_id, :created_at ]
t.index [ :session_window_id, :created_at ]
end
end
end
18 changes: 18 additions & 0 deletions demo/app/controllers/examples_controller.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,22 @@
class ExamplesController < ApplicationController
def index
end

def new
@resource = FakeModel.new
end

def create
resource_params = params.require(:resource).permit(:name, :message)
@resource = FakeModel.new(resource_params)
end

class FakeModel
include ActiveModel::Model
attr_accessor :name, :message

def self.model_name
ActiveModel::Name.new(self, nil, "Resource")
end
end
end
2 changes: 2 additions & 0 deletions demo/app/views/examples/create.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<%= @resource.name %>
<%= @resource.message %>
5 changes: 4 additions & 1 deletion demo/app/views/examples/index.html.erb
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
This is an example page.
<p>This is an example page to demonstrate <strong>Spectator Sport</strong></p>
<p>You are being recorded</p>

<%= link_to "Pretend you are doing stuff", { action: :new } %>
12 changes: 12 additions & 0 deletions demo/app/views/examples/new.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<%= form_with model: @resource, url: { action: :create }, action: :post do |f| %>
<%= f.label :name %>
<%= f.text_field :name %>

<br><br>
<%= f.label :message %>
<%= f.text_area :message %>

<br><br>

<%= f.submit "Submit" %>
<% end %>
4 changes: 4 additions & 0 deletions demo/app/views/layouts/application.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,9 @@

<body>
<%= yield %>

<br><br><br>
<hr>
<p>Replay it on the <%= link_to "Spectator Sport Dashboard", spectator_sport_path %></p>
</body>
</html>
2 changes: 2 additions & 0 deletions demo/config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@
mount SpectatorSport::Engine => "/spectator_sport"

root to: "examples#index"

resources :examples, only: [ :index, :show, :new, :create ]
end
23 changes: 21 additions & 2 deletions demo/db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,27 @@
create_table "spectator_sport_events", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "page_id", null: false
t.integer "session_id", null: false
t.integer "session_window_id"
t.json "event_data", null: false
t.index ["page_id", "created_at"], name: "index_spectator_sport_events_on_page_id_and_created_at"
t.index [ "session_id", "created_at" ], name: "index_spectator_sport_events_on_session_id_and_created_at"
t.index [ "session_id" ], name: "index_spectator_sport_events_on_session_id"
t.index [ "session_window_id", "created_at" ], name: "idx_on_session_window_id_created_at_f1aab0a880"
t.index [ "session_window_id" ], name: "index_spectator_sport_events_on_session_window_id"
end

create_table "spectator_sport_session_windows", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "session_id", null: false
t.string "secure_id", null: false
t.index [ "session_id" ], name: "index_spectator_sport_session_windows_on_session_id"
end

create_table "spectator_sport_sessions", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "secure_id", null: false
t.index [ "secure_id", "created_at" ], name: "index_spectator_sport_sessions_on_secure_id_and_created_at"
end
end
2 changes: 1 addition & 1 deletion lib/spectator_sport/engine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module SpectatorSport
class Engine < ::Rails::Engine
isolate_namespace SpectatorSport

initializer 'local_helper.action_controller' do
initializer "local_helper.action_controller" do
ActiveSupport.on_load :action_controller do
# TODO: this should probably be done manually by the client, maybe?
helper SpectatorSport::ScriptHelper
Expand Down

0 comments on commit e7b974e

Please sign in to comment.