Aggiunge heatmaps first-party (click/scroll) con pagina admin Analytics.
Raccoglie eventi aggregati dietro consenso cookie, senza PII né tracking su admin/regia/live/replay; GA4 resta invariato. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
<% content_for :body_class, "admin-body" %>
|
||||
|
||||
<div class="admin-page-head">
|
||||
<h2 class="admin-page-title"><%= t("admin.analytics.index.title") %></h2>
|
||||
<p class="muted admin-page-sub"><%= t("admin.analytics.index.lead") %></p>
|
||||
</div>
|
||||
|
||||
<div class="panel admin-sessions-filters">
|
||||
<%= form_with url: admin_analytics_path, method: :get, local: true, class: "admin-filter-form" do %>
|
||||
<div class="admin-filter-grid">
|
||||
<label class="admin-filter-field">
|
||||
<span><%= t("admin.analytics.filters.from") %></span>
|
||||
<%= date_field_tag :from, @filters[:from] %>
|
||||
</label>
|
||||
<label class="admin-filter-field">
|
||||
<span><%= t("admin.analytics.filters.to") %></span>
|
||||
<%= date_field_tag :to, @filters[:to] %>
|
||||
</label>
|
||||
<label class="admin-filter-field">
|
||||
<span><%= t("admin.analytics.filters.device") %></span>
|
||||
<%= select_tag :device,
|
||||
options_for_select(
|
||||
[[t("admin.analytics.filters.any"), ""]] + AnalyticsEvent::DEVICES.map { |d| [d, d] },
|
||||
@filters[:device]
|
||||
) %>
|
||||
</label>
|
||||
</div>
|
||||
<div class="admin-filter-actions">
|
||||
<%= submit_tag t("admin.analytics.filters.apply"), class: "admin-btn admin-btn--primary admin-btn--sm" %>
|
||||
<%= link_to t("admin.analytics.filters.reset"), admin_analytics_path, class: "admin-btn admin-btn--outline admin-btn--sm" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<% if @pages.any? %>
|
||||
<div class="admin-table-wrap">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><%= t("admin.analytics.index.table.path") %></th>
|
||||
<th><%= t("admin.analytics.index.table.pageviews") %></th>
|
||||
<th><%= t("admin.analytics.index.table.clicks") %></th>
|
||||
<th><%= t("admin.analytics.index.table.avg_scroll") %></th>
|
||||
<th><%= t("admin.analytics.index.table.max_scroll") %></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% @pages.each do |row| %>
|
||||
<% avg = row[:scroll_samples].positive? ? (row[:scroll_sum].to_f / row[:scroll_samples]).round : 0 %>
|
||||
<tr>
|
||||
<td><code class="admin-mono"><%= row[:page_path] %></code></td>
|
||||
<td><%= row[:pageviews] %></td>
|
||||
<td><%= row[:clicks] %></td>
|
||||
<td class="muted"><%= avg %>%</td>
|
||||
<td class="muted"><%= row[:max_scroll] %>%</td>
|
||||
<td>
|
||||
<%= link_to t("admin.analytics.index.heatmap"),
|
||||
admin_analytics_page_path(page_path: row[:page_path], from: @filters[:from], to: @filters[:to], device: @filters[:device]) %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="empty"><%= t("admin.analytics.index.none") %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -0,0 +1,115 @@
|
||||
<% content_for :body_class, "admin-body" %>
|
||||
|
||||
<div class="admin-page-head admin-session-head">
|
||||
<div>
|
||||
<p class="muted admin-page-sub">
|
||||
<%= link_to t("admin.analytics.show.back"), admin_analytics_path(from: @filters[:from], to: @filters[:to], device: @filters[:device]) %>
|
||||
</p>
|
||||
<h2 class="admin-page-title"><%= t("admin.analytics.show.title") %></h2>
|
||||
<p><code class="admin-mono"><%= @page_path %></code></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel admin-sessions-filters">
|
||||
<%= form_with url: admin_analytics_page_path, method: :get, local: true, class: "admin-filter-form" do %>
|
||||
<%= hidden_field_tag :page_path, @page_path %>
|
||||
<div class="admin-filter-grid">
|
||||
<label class="admin-filter-field">
|
||||
<span><%= t("admin.analytics.filters.from") %></span>
|
||||
<%= date_field_tag :from, @filters[:from] %>
|
||||
</label>
|
||||
<label class="admin-filter-field">
|
||||
<span><%= t("admin.analytics.filters.to") %></span>
|
||||
<%= date_field_tag :to, @filters[:to] %>
|
||||
</label>
|
||||
<label class="admin-filter-field">
|
||||
<span><%= t("admin.analytics.filters.device") %></span>
|
||||
<%= select_tag :device,
|
||||
options_for_select(
|
||||
[[t("admin.analytics.filters.any"), ""]] + AnalyticsEvent::DEVICES.map { |d| [d, d] },
|
||||
@filters[:device]
|
||||
) %>
|
||||
</label>
|
||||
</div>
|
||||
<div class="admin-filter-actions">
|
||||
<%= submit_tag t("admin.analytics.filters.apply"), class: "admin-btn admin-btn--primary admin-btn--sm" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="admin-session-grid">
|
||||
<section class="panel">
|
||||
<h3><%= t("admin.analytics.show.summary") %></h3>
|
||||
<dl class="admin-kv">
|
||||
<div>
|
||||
<dt><%= t("admin.analytics.index.table.pageviews") %></dt>
|
||||
<dd><%= @pageviews %></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt><%= t("admin.analytics.index.table.clicks") %></dt>
|
||||
<dd><%= @cells.values.sum %></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt><%= t("admin.analytics.index.table.avg_scroll") %></dt>
|
||||
<dd><%= @avg_scroll %>%</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt><%= t("admin.analytics.index.table.max_scroll") %></dt>
|
||||
<dd><%= @max_scroll %>%</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section class="panel" style="grid-column: span 2;">
|
||||
<h3><%= t("admin.analytics.show.scroll_depth") %></h3>
|
||||
<div class="admin-scroll-bar" aria-hidden="true">
|
||||
<div class="admin-scroll-bar__avg" style="width: <%= @avg_scroll %>%"></div>
|
||||
<div class="admin-scroll-bar__max" style="left: <%= @max_scroll %>%"></div>
|
||||
</div>
|
||||
<p class="muted admin-table-sub">
|
||||
<%= t("admin.analytics.show.scroll_hint", avg: @avg_scroll, max: @max_scroll) %>
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="panel">
|
||||
<h3><%= t("admin.analytics.show.heatmap_title") %></h3>
|
||||
<% if @cells.any? %>
|
||||
<div class="admin-heatmap-wrap">
|
||||
<canvas
|
||||
id="admin-heatmap"
|
||||
class="admin-heatmap"
|
||||
width="800"
|
||||
height="1200"
|
||||
data-grid="<%= @grid %>"
|
||||
data-max="<%= @max_clicks %>"
|
||||
data-cells="<%= @cells.map { |(x, y), c| { x: x, y: y, c: c } }.to_json %>"
|
||||
></canvas>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
var canvas = document.getElementById("admin-heatmap");
|
||||
if (!canvas) return;
|
||||
var grid = parseInt(canvas.getAttribute("data-grid"), 10) || 40;
|
||||
var max = parseInt(canvas.getAttribute("data-max"), 10) || 1;
|
||||
var cells = [];
|
||||
try { cells = JSON.parse(canvas.getAttribute("data-cells") || "[]"); } catch (e) { cells = []; }
|
||||
var ctx = canvas.getContext("2d");
|
||||
var cw = canvas.width;
|
||||
var ch = canvas.height;
|
||||
ctx.fillStyle = "#0d0d12";
|
||||
ctx.fillRect(0, 0, cw, ch);
|
||||
var cellW = cw / grid;
|
||||
var cellH = ch / grid;
|
||||
cells.forEach(function (cell) {
|
||||
var intensity = Math.min(1, cell.c / max);
|
||||
var alpha = 0.15 + intensity * 0.75;
|
||||
ctx.fillStyle = "rgba(229, 57, 53, " + alpha + ")";
|
||||
ctx.fillRect(cell.x * cellW, cell.y * cellH, cellW + 0.5, cellH + 0.5);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<% else %>
|
||||
<p class="empty"><%= t("admin.analytics.show.no_clicks") %></p>
|
||||
<% end %>
|
||||
</section>
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<%= csrf_meta_tags %>
|
||||
<link rel="stylesheet" href="/admin.css?v=12">
|
||||
<link rel="stylesheet" href="/admin.css?v=13">
|
||||
<% if content_for?(:replay_archive_styles) %>
|
||||
<link rel="stylesheet" href="/marketing.css?v=42">
|
||||
<% end %>
|
||||
@@ -31,6 +31,7 @@
|
||||
<%= link_to t("admin.layout.nav.billing"), admin_billing_path, class: ("active" if controller_name.in?(%w[billing billing_invoices])) %>
|
||||
<%= link_to t("admin.layout.nav.youtube"), admin_youtube_platform_path, class: ("active" if controller_name == "youtube") %>
|
||||
<%= link_to t("admin.layout.nav.sessions"), admin_sessions_path, class: ("active" if controller_name == "sessions") %>
|
||||
<%= link_to t("admin.layout.nav.analytics"), admin_analytics_path, class: ("active" if controller_name == "analytics") %>
|
||||
<%= link_to t("admin.layout.nav.stream_nodes"), admin_stream_nodes_path, class: ("active" if controller_name == "stream_nodes") %>
|
||||
<%= link_to t("admin.layout.nav.password"), edit_admin_password_path %>
|
||||
<%= button_to t("admin.layout.nav.logout"), admin_logout_path, method: :delete %>
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
<script src="/password-toggle.js?v=2" defer></script>
|
||||
<link rel="stylesheet" href="/confirm-forms.css?v=4">
|
||||
<script src="/confirm-forms.js?v=7" defer></script>
|
||||
<script src="/cookie-consent.js?v=1" defer></script>
|
||||
<script src="/site-analytics.js?v=1" defer></script>
|
||||
<script src="/cookie-consent.js?v=2" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
<%= render "shared/marketing_footer" %>
|
||||
<link rel="stylesheet" href="/confirm-forms.css?v=4">
|
||||
<script src="/confirm-forms.js?v=7" defer></script>
|
||||
<script src="/cookie-consent.js?v=1" defer></script>
|
||||
<script src="/site-analytics.js?v=1" defer></script>
|
||||
<script src="/cookie-consent.js?v=2" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -101,6 +101,12 @@
|
||||
<td><%= t("legal.cookies.s4_2_row3_duration") %></td>
|
||||
<td><%= t("legal.cookies.s4_2_row3_provider") %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><%= raw t("legal.cookies.s4_2_row4_name_html") %></td>
|
||||
<td><%= t("legal.cookies.s4_2_row4_purpose") %></td>
|
||||
<td><%= t("legal.cookies.s4_2_row4_duration") %></td>
|
||||
<td><%= t("legal.cookies.s4_2_row4_provider") %></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user