<%= content_for :title, "Analytics" %>
|
|
|
|
<%= turbo_frame_tag 'main' do %>
|
|
|
|
<%= turbo_stream.append 'flash', partial: 'layouts/flash' %>
|
|
|
|
<div class="list-title">
|
|
<h1><%= yield(:title) %></h1>
|
|
|
|
<div class="analytics-periods">
|
|
<% { "all" => "All time", "30d" => "30 days", "7d" => "7 days", "24h" => "24 hours" }.each do |key, label| %>
|
|
<%= link_to label,
|
|
url_for(period: key),
|
|
class: class_names("analytics-period", current: period == key),
|
|
data: { turbo_frame: "main", turbo_action: "advance" } %>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="analytics">
|
|
|
|
<% if DemoActivity::ENABLED %>
|
|
<p class="analytics-warning">
|
|
<strong>Demo data is on.</strong>
|
|
<code>DemoActivity</code> keeps inventing players with no gameplay to feed the leaderboard
|
|
banner, so the counts below are inflated and the funnel shows a large drop before "Facts".
|
|
Set <code>DemoActivity::ENABLED = false</code> and wipe the players table before go-live.
|
|
</p>
|
|
<% end %>
|
|
|
|
<section class="analytics-cards">
|
|
<%= render "card", label: "Players started", value: number_with_delimiter(@analytics.total_players) %>
|
|
<%= render "card", label: "Finished", value: number_with_delimiter(@analytics.completed_players),
|
|
sub: "#{@analytics.completion_rate}% of starters" %>
|
|
<%= render "card", label: "Median play time", value: format_duration(@analytics.median_duration) %>
|
|
<%= render "card", label: "Average score", value: (@analytics.average_score || "—") %>
|
|
</section>
|
|
|
|
|
|
<section class="analytics-panel">
|
|
<h2>Funnel</h2>
|
|
<p class="analytics-note">
|
|
How far players got. The last-save early exit is a branch off the stages, so it is
|
|
reported separately below rather than as a funnel step.
|
|
</p>
|
|
|
|
<div class="analytics-bars">
|
|
<% @analytics.funnel.each do |row| %>
|
|
<div class="analytics-bar-row">
|
|
<div class="analytics-bar-label"><%= row[:label] %></div>
|
|
<div class="analytics-bar-track">
|
|
<div class="analytics-bar-fill" style="width: <%= row[:share] %>%"></div>
|
|
</div>
|
|
<div class="analytics-bar-value">
|
|
<strong><%= number_with_delimiter(row[:reached]) %></strong>
|
|
<span><%= row[:share] %>%</span>
|
|
</div>
|
|
<div class="analytics-bar-delta">
|
|
<% if row[:lost] > 0 %>
|
|
<span class="negative">-<%= number_with_delimiter(row[:lost]) %> (<%= row[:lost_share] %>%)</span>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</section>
|
|
|
|
|
|
<section class="analytics-panel">
|
|
<h2>Where unfinished players stopped</h2>
|
|
|
|
<% if @analytics.drop_off_points.empty? %>
|
|
<p class="analytics-empty">Nobody has abandoned a game yet.</p>
|
|
<% else %>
|
|
<div class="analytics-bars">
|
|
<% max = @analytics.drop_off_points.first[:count] %>
|
|
<% @analytics.drop_off_points.each do |row| %>
|
|
<div class="analytics-bar-row">
|
|
<div class="analytics-bar-label"><%= row[:label] %></div>
|
|
<div class="analytics-bar-track">
|
|
<div class="analytics-bar-fill negative" style="width: <%= (row[:count] * 100.0 / max).round(1) %>%"></div>
|
|
</div>
|
|
<div class="analytics-bar-value"><strong><%= number_with_delimiter(row[:count]) %></strong></div>
|
|
<div class="analytics-bar-delta"></div>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
<% end %>
|
|
</section>
|
|
|
|
|
|
<section class="analytics-panel">
|
|
<h2>Answers by stage</h2>
|
|
|
|
<% @analytics.stages.each do |stage| %>
|
|
<div class="analytics-stage">
|
|
<h3>
|
|
<span class="analytics-stage-index"><%= stage[:index] %></span>
|
|
<%= stage[:node].title %>
|
|
<small><%= number_with_delimiter(stage[:total]) %> answers</small>
|
|
</h3>
|
|
|
|
<div class="analytics-bars">
|
|
<% stage[:answers].each do |answer| %>
|
|
<div class="analytics-bar-row">
|
|
<div class="analytics-bar-label">
|
|
<%= answer[:label] %>
|
|
<%= tag.em(answer[:node].template.humanize) %>
|
|
</div>
|
|
<div class="analytics-bar-track">
|
|
<div class="analytics-bar-fill <%= answer_tone(answer[:node]) %>" style="width: <%= answer[:share] %>%"></div>
|
|
</div>
|
|
<div class="analytics-bar-value">
|
|
<strong><%= number_with_delimiter(answer[:count]) %></strong>
|
|
<span><%= answer[:share] %>%</span>
|
|
</div>
|
|
<div class="analytics-bar-delta"></div>
|
|
</div>
|
|
|
|
<% answer[:outcomes].each do |outcome| %>
|
|
<div class="analytics-bar-row is-outcome">
|
|
<div class="analytics-bar-label">
|
|
↳ <%= outcome[:label] %>
|
|
<%= tag.em(outcome[:node].template.humanize) %>
|
|
</div>
|
|
<div class="analytics-bar-track">
|
|
<div class="analytics-bar-fill <%= answer_tone(outcome[:node]) %>" style="width: <%= outcome[:share] %>%"></div>
|
|
</div>
|
|
<div class="analytics-bar-value">
|
|
<strong><%= number_with_delimiter(outcome[:count]) %></strong>
|
|
<span><%= outcome[:share] %>% of chances</span>
|
|
</div>
|
|
<div class="analytics-bar-delta"></div>
|
|
</div>
|
|
<% end %>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
</section>
|
|
|
|
|
|
<% if @analytics.last_save[:node] %>
|
|
<section class="analytics-panel">
|
|
<h2>Last save <small>early exit — <%= number_with_delimiter(@analytics.last_save[:total]) %> players</small></h2>
|
|
|
|
<% if @analytics.last_save[:total].zero? %>
|
|
<p class="analytics-empty">No player has taken the early exit yet.</p>
|
|
<% else %>
|
|
<div class="analytics-bars">
|
|
<% @analytics.last_save[:answers].each do |answer| %>
|
|
<div class="analytics-bar-row">
|
|
<div class="analytics-bar-label"><%= answer[:label] %></div>
|
|
<div class="analytics-bar-track">
|
|
<div class="analytics-bar-fill" style="width: <%= answer[:share] %>%"></div>
|
|
</div>
|
|
<div class="analytics-bar-value">
|
|
<strong><%= number_with_delimiter(answer[:count]) %></strong>
|
|
<span><%= answer[:share] %>%</span>
|
|
</div>
|
|
<div class="analytics-bar-delta"></div>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
<% end %>
|
|
</section>
|
|
<% end %>
|
|
|
|
|
|
<section class="analytics-panel">
|
|
<h2>Endings <small>finished players, excluding early exits</small></h2>
|
|
|
|
<div class="analytics-bars">
|
|
<% @analytics.result_bands.each do |band| %>
|
|
<div class="analytics-bar-row">
|
|
<div class="analytics-bar-label"><%= t("game.results.#{band[:band]}.headline", default: band[:band].to_s.humanize).html_safe %></div>
|
|
<div class="analytics-bar-track">
|
|
<div class="analytics-bar-fill <%= band_tone(band[:band]) %>" style="width: <%= band[:share] %>%"></div>
|
|
</div>
|
|
<div class="analytics-bar-value">
|
|
<strong><%= number_with_delimiter(band[:count]) %></strong>
|
|
<span><%= band[:share] %>%</span>
|
|
</div>
|
|
<div class="analytics-bar-delta"></div>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</section>
|
|
|
|
|
|
<%= render "breakdown", title: "How they got here",
|
|
note: "Where the scan came from. Each printed QR code carries its own " \
|
|
"utm_content tag, so the rows below are the physical placements. " \
|
|
"The board game QR points at dice.ikeafoundation.org and is counted there, not here.",
|
|
rows: @analytics.by_source,
|
|
labeller: ->(key) { source_breakdown_label(key) } %>
|
|
|
|
|
|
<div class="analytics-columns">
|
|
<%= render "breakdown", title: "Devices", rows: @analytics.by_device,
|
|
labeller: ->(key) { device_breakdown_label(key) } %>
|
|
<%= render "breakdown", title: "Languages", rows: @analytics.by_locale,
|
|
labeller: ->(key) { language_breakdown_label(key) } %>
|
|
<%= render "breakdown", title: "Countries", rows: @analytics.by_country,
|
|
labeller: ->(key) { country_breakdown_label(key) } %>
|
|
</div>
|
|
|
|
</div>
|
|
<% end %>
|