sentiero 1.0.0.alpha1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +7 -0
- data/README.md +679 -0
- data/lib/sentiero/analytics/analyzer.rb +91 -0
- data/lib/sentiero/analytics/bounded.rb +29 -0
- data/lib/sentiero/analytics/browser_event_discovery.rb +70 -0
- data/lib/sentiero/analytics/collectors/click_collector.rb +135 -0
- data/lib/sentiero/analytics/collectors/custom_tag_collector.rb +61 -0
- data/lib/sentiero/analytics/collectors/error_collector.rb +89 -0
- data/lib/sentiero/analytics/collectors/form_collector.rb +156 -0
- data/lib/sentiero/analytics/collectors/frustration_collector.rb +85 -0
- data/lib/sentiero/analytics/collectors/scroll_collector.rb +156 -0
- data/lib/sentiero/analytics/collectors/vitals_collector.rb +104 -0
- data/lib/sentiero/analytics/conversion_analyzer.rb +247 -0
- data/lib/sentiero/analytics/engagement_analyzer.rb +331 -0
- data/lib/sentiero/analytics/entry_attribution.rb +71 -0
- data/lib/sentiero/analytics/error_discovery.rb +118 -0
- data/lib/sentiero/analytics/events.rb +21 -0
- data/lib/sentiero/analytics/exporter.rb +242 -0
- data/lib/sentiero/analytics/form_analyzer.rb +153 -0
- data/lib/sentiero/analytics/frustration/detectors.rb +158 -0
- data/lib/sentiero/analytics/frustration_analyzer.rb +235 -0
- data/lib/sentiero/analytics/funnel_analyzer.rb +160 -0
- data/lib/sentiero/analytics/heatmap_analyzer.rb +93 -0
- data/lib/sentiero/analytics/page_report_analyzer.rb +198 -0
- data/lib/sentiero/analytics/problem_detail.rb +97 -0
- data/lib/sentiero/analytics/scroll_depth_analyzer.rb +30 -0
- data/lib/sentiero/analytics/segmenter.rb +133 -0
- data/lib/sentiero/analytics/server_event_metrics.rb +120 -0
- data/lib/sentiero/analytics/stats.rb +30 -0
- data/lib/sentiero/analytics/stats_aggregator/result_builder.rb +153 -0
- data/lib/sentiero/analytics/stats_aggregator.rb +346 -0
- data/lib/sentiero/analytics/web_vitals_analyzer.rb +57 -0
- data/lib/sentiero/configuration.rb +184 -0
- data/lib/sentiero/erasure.rb +48 -0
- data/lib/sentiero/fingerprint.rb +34 -0
- data/lib/sentiero/ip_anonymizer.rb +29 -0
- data/lib/sentiero/redaction/config.rb +61 -0
- data/lib/sentiero/redaction.rb +207 -0
- data/lib/sentiero/reporter/configuration.rb +50 -0
- data/lib/sentiero/reporter/context.rb +31 -0
- data/lib/sentiero/reporter/dispatcher.rb +91 -0
- data/lib/sentiero/reporter/http_transport.rb +57 -0
- data/lib/sentiero/reporter/log_transport.rb +26 -0
- data/lib/sentiero/reporter/middleware.rb +62 -0
- data/lib/sentiero/reporter/normalizer.rb +14 -0
- data/lib/sentiero/reporter/null_transport.rb +18 -0
- data/lib/sentiero/reporter/report_context.rb +29 -0
- data/lib/sentiero/reporter/scrubber.rb +47 -0
- data/lib/sentiero/reporter/test_helper.rb +32 -0
- data/lib/sentiero/reporter/test_transport.rb +28 -0
- data/lib/sentiero/reporter.rb +214 -0
- data/lib/sentiero/roda.rb +47 -0
- data/lib/sentiero/store/error_store.rb +220 -0
- data/lib/sentiero/store/limits.rb +31 -0
- data/lib/sentiero/store/session_store.rb +118 -0
- data/lib/sentiero/store.rb +72 -0
- data/lib/sentiero/stores/file.rb +566 -0
- data/lib/sentiero/stores/memory.rb +362 -0
- data/lib/sentiero/stores/redis/keys.rb +59 -0
- data/lib/sentiero/stores/redis/lua.rb +119 -0
- data/lib/sentiero/stores/redis.rb +665 -0
- data/lib/sentiero/stores/sqlite/schema.rb +79 -0
- data/lib/sentiero/stores/sqlite.rb +626 -0
- data/lib/sentiero/user_agent.rb +32 -0
- data/lib/sentiero/version.rb +5 -0
- data/lib/sentiero/web/analytics_app.rb +538 -0
- data/lib/sentiero/web/assets/analytics-RH24EOLD.js +1 -0
- data/lib/sentiero/web/assets/dashboard-JFYNHZZV.js +3 -0
- data/lib/sentiero/web/assets/heatmap-EBKFWSKN.js +1 -0
- data/lib/sentiero/web/assets/import-HIMBJJ4S.js +1 -0
- data/lib/sentiero/web/assets/manifest.json +11 -0
- data/lib/sentiero/web/assets/recorder-SLLXSUUX.js +71 -0
- data/lib/sentiero/web/assets/rrweb-player-cd435a95.js +126 -0
- data/lib/sentiero/web/assets/rrweb-player-css-ce5e9629.css +2 -0
- data/lib/sentiero/web/assets/sessions_index-2RAGTEZM.js +1 -0
- data/lib/sentiero/web/assets/style-d71e72fd.css +2 -0
- data/lib/sentiero/web/assets_app.rb +42 -0
- data/lib/sentiero/web/base_app.rb +319 -0
- data/lib/sentiero/web/basic_auth.rb +27 -0
- data/lib/sentiero/web/basic_auth_check.rb +41 -0
- data/lib/sentiero/web/body_reader.rb +44 -0
- data/lib/sentiero/web/csv_writer.rb +45 -0
- data/lib/sentiero/web/dashboard_app.rb +236 -0
- data/lib/sentiero/web/errors_app.rb +97 -0
- data/lib/sentiero/web/escaping.rb +37 -0
- data/lib/sentiero/web/events_app.rb +196 -0
- data/lib/sentiero/web/formatting.rb +43 -0
- data/lib/sentiero/web/ingest_app.rb +92 -0
- data/lib/sentiero/web/manifest.rb +43 -0
- data/lib/sentiero/web/monitoring_app.rb +316 -0
- data/lib/sentiero/web/script_tag.rb +57 -0
- data/lib/sentiero/web/shareable_replay.rb +88 -0
- data/lib/sentiero/web/templates/_analytics_nav.html.erb +22 -0
- data/lib/sentiero/web/templates/_brand.html.erb +18 -0
- data/lib/sentiero/web/templates/_date_range.html.erb +18 -0
- data/lib/sentiero/web/templates/_errors_client_filter.html.erb +25 -0
- data/lib/sentiero/web/templates/_errors_server_filter.html.erb +36 -0
- data/lib/sentiero/web/templates/_events_browser_filter.html.erb +18 -0
- data/lib/sentiero/web/templates/_events_server_filter.html.erb +39 -0
- data/lib/sentiero/web/templates/_pagination.html.erb +14 -0
- data/lib/sentiero/web/templates/_payload_metrics.html.erb +62 -0
- data/lib/sentiero/web/templates/_session_row.html.erb +42 -0
- data/lib/sentiero/web/templates/_sibling_tab_hint.html.erb +6 -0
- data/lib/sentiero/web/templates/_tabs.html.erb +10 -0
- data/lib/sentiero/web/templates/_truncation_warning.html.erb +19 -0
- data/lib/sentiero/web/templates/_window_tab.html.erb +5 -0
- data/lib/sentiero/web/templates/analytics_conversions.html.erb +94 -0
- data/lib/sentiero/web/templates/analytics_engagement.html.erb +101 -0
- data/lib/sentiero/web/templates/analytics_frustration.html.erb +135 -0
- data/lib/sentiero/web/templates/analytics_funnel.html.erb +103 -0
- data/lib/sentiero/web/templates/analytics_index.html.erb +380 -0
- data/lib/sentiero/web/templates/analytics_page.html.erb +287 -0
- data/lib/sentiero/web/templates/analytics_scroll.html.erb +94 -0
- data/lib/sentiero/web/templates/analytics_vitals.html.erb +91 -0
- data/lib/sentiero/web/templates/client_error_show.html.erb +73 -0
- data/lib/sentiero/web/templates/dashboard.html.erb +56 -0
- data/lib/sentiero/web/templates/errors_index.html.erb +149 -0
- data/lib/sentiero/web/templates/event_show.html.erb +52 -0
- data/lib/sentiero/web/templates/events_index.html.erb +177 -0
- data/lib/sentiero/web/templates/export_index.html.erb +69 -0
- data/lib/sentiero/web/templates/forms.html.erb +105 -0
- data/lib/sentiero/web/templates/heatmap.html.erb +76 -0
- data/lib/sentiero/web/templates/import.html.erb +39 -0
- data/lib/sentiero/web/templates/problem_show.html.erb +200 -0
- data/lib/sentiero/web/templates/segments.html.erb +114 -0
- data/lib/sentiero/web/templates/session_show.html.erb +195 -0
- data/lib/sentiero/web/templates/sessions_index.html.erb +97 -0
- data/lib/sentiero/web/track_app.rb +57 -0
- data/lib/sentiero/web/views/analytics_index_view.rb +86 -0
- data/lib/sentiero/web/views/analyzer_view.rb +27 -0
- data/lib/sentiero/web/views/base_view.rb +76 -0
- data/lib/sentiero/web/views/client_error_show_view.rb +29 -0
- data/lib/sentiero/web/views/conversions_view.rb +41 -0
- data/lib/sentiero/web/views/engagement_view.rb +67 -0
- data/lib/sentiero/web/views/errors_index_view.rb +37 -0
- data/lib/sentiero/web/views/event_show_view.rb +20 -0
- data/lib/sentiero/web/views/events_index_view.rb +56 -0
- data/lib/sentiero/web/views/export_view.rb +23 -0
- data/lib/sentiero/web/views/forms_view.rb +28 -0
- data/lib/sentiero/web/views/frustration_view.rb +15 -0
- data/lib/sentiero/web/views/funnel_view.rb +36 -0
- data/lib/sentiero/web/views/heatmap_view.rb +34 -0
- data/lib/sentiero/web/views/import_view.rb +13 -0
- data/lib/sentiero/web/views/page_report_view.rb +43 -0
- data/lib/sentiero/web/views/problem_show_view.rb +46 -0
- data/lib/sentiero/web/views/scroll_view.rb +23 -0
- data/lib/sentiero/web/views/segments_view.rb +28 -0
- data/lib/sentiero/web/views/session_show_view.rb +105 -0
- data/lib/sentiero/web/views/sessions_index_view.rb +28 -0
- data/lib/sentiero/web/views/vitals_view.rb +45 -0
- data/lib/sentiero/web/views.rb +24 -0
- data/lib/sentiero/window_ref.rb +6 -0
- data/lib/sentiero.rb +69 -0
- metadata +232 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<div class="flex items-center justify-between mb-5">
|
|
2
|
+
<h1 class="page-title mb-0">Import Replay</h1>
|
|
3
|
+
<a href="<%= view.h(view.base_path) %>/analytics/export" class="btn btn-sm btn-secondary">← Back to Export</a>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<div class="card mb-4">
|
|
7
|
+
<div class="card-body">
|
|
8
|
+
<p class="text-xs text-gray-500 mb-4">
|
|
9
|
+
Paste a session events JSON file below, or drag and drop one onto the box.
|
|
10
|
+
The replay runs entirely in your browser — nothing is uploaded.
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
<div id="import-dropzone" class="border-2 border-dashed border-gray-200 rounded p-4 transition-colors">
|
|
14
|
+
<label for="import-textarea" class="label">Events JSON</label>
|
|
15
|
+
<textarea id="import-textarea" class="select w-full font-mono text-xs" rows="6"
|
|
16
|
+
placeholder='[{"type":4,"timestamp":...}, ...]'></textarea>
|
|
17
|
+
|
|
18
|
+
<div class="flex items-center gap-2 mt-3">
|
|
19
|
+
<button id="import-replay" type="button" class="btn btn-sm btn-secondary">Replay</button>
|
|
20
|
+
<input id="import-file" type="file" accept="application/json,.json" class="text-xs">
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<p id="import-status" class="text-xs text-gray-400 mt-3" role="status" aria-live="polite"></p>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<div class="card">
|
|
29
|
+
<div class="card-body">
|
|
30
|
+
<h6 class="text-xs font-semibold text-gray-800 uppercase tracking-wider mb-3">Replay</h6>
|
|
31
|
+
<div class="player-frame">
|
|
32
|
+
<div id="import-player"></div>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<link rel="stylesheet" href="<%= view.built_asset('rrweb-player-css') %>">
|
|
38
|
+
<script src="<%= view.built_asset('rrweb-player') %>"></script>
|
|
39
|
+
<script src="<%= view.built_asset('import') %>" defer></script>
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
<div class="card mb-3">
|
|
2
|
+
<div class="flex items-center gap-2 px-4 py-2.5">
|
|
3
|
+
<a href="<%= view.h(view.base_path) %>/issues" class="btn btn-sm btn-secondary shrink-0">← Back</a>
|
|
4
|
+
<div class="w-px h-4 bg-gray-200 shrink-0"></div>
|
|
5
|
+
<span class="badge badge-neutral shrink-0"><%= view.h(view.problem[:exception_class].to_s) %></span>
|
|
6
|
+
<% case view.problem[:status]
|
|
7
|
+
when "resolved" -%>
|
|
8
|
+
<span class="badge badge-success shrink-0">resolved</span>
|
|
9
|
+
<% when "ignored" -%>
|
|
10
|
+
<span class="badge badge-neutral shrink-0">ignored</span>
|
|
11
|
+
<% else -%>
|
|
12
|
+
<span class="badge badge-danger shrink-0">open</span>
|
|
13
|
+
<% end -%>
|
|
14
|
+
<span class="badge badge-neutral shrink-0"><%= view.problem[:count] %> occurrence<%= view.problem[:count] == 1 ? "" : "s" %></span>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<div class="card mb-3">
|
|
19
|
+
<div class="card-body">
|
|
20
|
+
<h5 class="text-sm font-semibold text-gray-800 mb-2"><%= view.h(view.problem[:title].to_s) %></h5>
|
|
21
|
+
<div class="grid grid-cols-[100px_1fr] gap-x-4 gap-y-2 mt-3">
|
|
22
|
+
<div class="text-[10px] font-medium text-gray-400 uppercase tracking-wider pt-0.5">Project</div>
|
|
23
|
+
<div class="text-xs text-gray-600"><%= view.h(view.problem[:project].to_s) %></div>
|
|
24
|
+
|
|
25
|
+
<div class="text-[10px] font-medium text-gray-400 uppercase tracking-wider pt-0.5">First seen</div>
|
|
26
|
+
<div class="text-xs text-gray-600"><%= view.problem[:first_seen] ? Time.at(view.problem[:first_seen]).utc.strftime("%b %d, %Y %H:%M:%S UTC") : "N/A" %></div>
|
|
27
|
+
|
|
28
|
+
<div class="text-[10px] font-medium text-gray-400 uppercase tracking-wider pt-0.5">Last seen</div>
|
|
29
|
+
<div class="text-xs text-gray-600"><%= view.problem[:last_seen] ? Time.at(view.problem[:last_seen]).utc.strftime("%b %d, %Y %H:%M:%S UTC") : "N/A" %></div>
|
|
30
|
+
|
|
31
|
+
<div class="text-[10px] font-medium text-gray-400 uppercase tracking-wider pt-0.5">Fingerprint</div>
|
|
32
|
+
<div class="text-xs text-gray-600 font-mono"><%= view.h(view.problem[:id].to_s) %></div>
|
|
33
|
+
|
|
34
|
+
<% if view.problem[:resolved_at] -%>
|
|
35
|
+
<div class="text-[10px] font-medium text-gray-400 uppercase tracking-wider pt-0.5">Resolved at</div>
|
|
36
|
+
<div class="text-xs text-gray-600"><%= Time.at(view.problem[:resolved_at]).utc.strftime("%b %d, %Y %H:%M UTC") %></div>
|
|
37
|
+
<% end -%>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<hr class="my-4 border-gray-100">
|
|
41
|
+
<h6 class="text-xs font-semibold text-gray-700 mb-3 uppercase tracking-wider">Update Status</h6>
|
|
42
|
+
<form method="post" action="<%= view.h(view.base_path) %>/issues/<%= view.h(view.problem[:id]) %>/status" class="flex gap-2 flex-wrap">
|
|
43
|
+
<input type="hidden" name="csrf_token" value="<%= view.h(view.csrf_token) %>">
|
|
44
|
+
<% unless view.problem[:status] == "open" -%>
|
|
45
|
+
<button type="submit" name="status" value="open" class="btn btn-sm btn-secondary">Reopen</button>
|
|
46
|
+
<% end -%>
|
|
47
|
+
<% unless view.problem[:status] == "resolved" -%>
|
|
48
|
+
<button type="submit" name="status" value="resolved" class="btn btn-sm btn-primary">Resolve</button>
|
|
49
|
+
<% end -%>
|
|
50
|
+
<% unless view.problem[:status] == "ignored" -%>
|
|
51
|
+
<button type="submit" name="status" value="ignored" class="btn btn-sm btn-secondary">Ignore</button>
|
|
52
|
+
<% end -%>
|
|
53
|
+
</form>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<% unless view.session_summaries.empty? -%>
|
|
58
|
+
<div class="card mb-3">
|
|
59
|
+
<div class="card-body">
|
|
60
|
+
<h6 class="text-xs font-semibold text-gray-800 mb-3 uppercase tracking-wider">Sessions Affected (<%= view.session_summaries.size %>)</h6>
|
|
61
|
+
<ul class="space-y-2">
|
|
62
|
+
<% view.session_summaries.each do |s| -%>
|
|
63
|
+
<li class="flex items-center gap-3 text-xs">
|
|
64
|
+
<a href="<%= view.replay_href(s) %>" class="text-blue-600 hover:text-blue-800 font-mono shrink-0"><%= view.h(s[:session_id]) %></a>
|
|
65
|
+
<% if s[:last_event_at] && s[:first_event_at] -%>
|
|
66
|
+
<span class="text-gray-400 shrink-0"><%= view.h(view.format_duration(s[:first_event_at], s[:last_event_at])) %></span>
|
|
67
|
+
<% end -%>
|
|
68
|
+
<% if s[:browser] -%>
|
|
69
|
+
<span class="text-gray-500 truncate"><%= view.h(s[:browser]) %></span>
|
|
70
|
+
<% end -%>
|
|
71
|
+
</li>
|
|
72
|
+
<% end -%>
|
|
73
|
+
</ul>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
<% end -%>
|
|
77
|
+
|
|
78
|
+
<%# B4 occurrence trend: exact 24h/7d/30d counts + a per-UTC-day sparkline
|
|
79
|
+
over the occurrences fetched for this page. -%>
|
|
80
|
+
<div class="card mb-3">
|
|
81
|
+
<div class="card-body">
|
|
82
|
+
<h6 class="text-xs font-semibold text-gray-800 mb-3 uppercase tracking-wider">Occurrence Trend</h6>
|
|
83
|
+
<div class="flex items-center gap-2 mb-3 flex-wrap">
|
|
84
|
+
<span class="badge badge-neutral">24h <span class="tabular-nums" data-trend-24h="<%= view.trend[:last_24h] %>"><%= view.trend[:last_24h] %></span></span>
|
|
85
|
+
<span class="badge badge-neutral">7d <span class="tabular-nums" data-trend-7d="<%= view.trend[:last_7d] %>"><%= view.trend[:last_7d] %></span></span>
|
|
86
|
+
<span class="badge badge-neutral">30d <span class="tabular-nums" data-trend-30d="<%= view.trend[:last_30d] %>"><%= view.trend[:last_30d] %></span></span>
|
|
87
|
+
</div>
|
|
88
|
+
<% if view.spark_max == 0 -%>
|
|
89
|
+
<p class="text-gray-400 text-xs py-4 text-center">No occurrences in the last 30 days.</p>
|
|
90
|
+
<% else -%>
|
|
91
|
+
<div class="stats-chart">
|
|
92
|
+
<% view.trend[:series].each do |day| -%>
|
|
93
|
+
<div class="stats-chart-col">
|
|
94
|
+
<div class="stats-chart-bar-wrapper">
|
|
95
|
+
<div class="stats-chart-bar" style="height: <%= (day[:count].to_f / view.spark_max * 100).round %>%" title="<%= view.h(day[:date]) %>: <%= day[:count] %> occurrence<%= day[:count] == 1 ? "" : "s" %>" data-spark-count="<%= day[:count] %>"></div>
|
|
96
|
+
</div>
|
|
97
|
+
<div class="stats-chart-label"><%= view.h(day[:date][-5..]) %></div>
|
|
98
|
+
</div>
|
|
99
|
+
<% end -%>
|
|
100
|
+
</div>
|
|
101
|
+
<div class="text-[10px] text-gray-400 mt-1">By UTC day — latest <%= view.trend[:sample_size] %> occurrences.</div>
|
|
102
|
+
<% end -%>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<% if view.has_facets? -%>
|
|
107
|
+
<div class="card mb-3" data-problem-facets>
|
|
108
|
+
<div class="card-body">
|
|
109
|
+
<h6 class="text-xs font-semibold text-gray-800 mb-3 uppercase tracking-wider">Facets
|
|
110
|
+
<span class="font-normal normal-case tracking-normal text-gray-400">— latest <%= view.facets[:sample_size] %> occurrences</span>
|
|
111
|
+
</h6>
|
|
112
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
113
|
+
<% view.facet_groups.each do |title, rows| -%>
|
|
114
|
+
<% next if rows.empty? -%>
|
|
115
|
+
<div>
|
|
116
|
+
<div class="text-[10px] font-medium text-gray-400 uppercase tracking-wider mb-1.5"><%= view.h(title) %></div>
|
|
117
|
+
<table class="w-full text-xs">
|
|
118
|
+
<% rows.each do |value, count| -%>
|
|
119
|
+
<tr class="border-b border-gray-100 last:border-0">
|
|
120
|
+
<td class="py-1 text-gray-700 truncate max-w-0 w-full"><%= view.h(value) %></td>
|
|
121
|
+
<td class="py-1 pl-2 text-right text-gray-500 tabular-nums" data-facet-count="<%= count %>"><%= count %></td>
|
|
122
|
+
</tr>
|
|
123
|
+
<% end -%>
|
|
124
|
+
</table>
|
|
125
|
+
</div>
|
|
126
|
+
<% end -%>
|
|
127
|
+
<% unless view.facets[:releases].empty? -%>
|
|
128
|
+
<div>
|
|
129
|
+
<div class="text-[10px] font-medium text-gray-400 uppercase tracking-wider mb-1.5">Releases</div>
|
|
130
|
+
<table class="w-full text-xs">
|
|
131
|
+
<% view.facets[:releases].each do |release, info| -%>
|
|
132
|
+
<tr class="border-b border-gray-100 last:border-0">
|
|
133
|
+
<td class="py-1 text-gray-700 truncate max-w-0 w-full">
|
|
134
|
+
<%= view.h(release) %>
|
|
135
|
+
<% if info[:first_seen] -%>
|
|
136
|
+
<span class="text-[10px] text-gray-400">· first seen <%= Time.at(info[:first_seen]).utc.strftime("%b %d, %H:%M") %></span>
|
|
137
|
+
<% end -%>
|
|
138
|
+
</td>
|
|
139
|
+
<td class="py-1 pl-2 text-right text-gray-500 tabular-nums" data-facet-count="<%= info[:count] %>"><%= info[:count] %></td>
|
|
140
|
+
</tr>
|
|
141
|
+
<% end -%>
|
|
142
|
+
</table>
|
|
143
|
+
</div>
|
|
144
|
+
<% end -%>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
<% end -%>
|
|
149
|
+
|
|
150
|
+
<% unless view.occurrences.empty? -%>
|
|
151
|
+
<div class="card">
|
|
152
|
+
<div class="card-body">
|
|
153
|
+
<h6 class="text-xs font-semibold text-gray-800 mb-3 uppercase tracking-wider">Recent Occurrences</h6>
|
|
154
|
+
<div class="space-y-4">
|
|
155
|
+
<% view.occurrences.each do |occ| -%>
|
|
156
|
+
<div class="border border-gray-100 rounded p-3">
|
|
157
|
+
<div class="flex items-start justify-between gap-2 mb-2">
|
|
158
|
+
<div class="text-xs text-gray-700 font-medium flex-1"><%= view.h(occ["message"].to_s) %></div>
|
|
159
|
+
<div class="text-[10px] text-gray-400 shrink-0 tabular-nums">
|
|
160
|
+
<%= occ["timestamp"] ? Time.at(occ["timestamp"].to_f).utc.strftime("%b %d, %H:%M:%S") : "N/A" %>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
<% if occ["session_id"] && !occ["session_id"].empty? -%>
|
|
164
|
+
<div class="text-[10px] text-gray-400 mb-2">
|
|
165
|
+
Session:
|
|
166
|
+
<% if occ["window_id"] && !occ["window_id"].empty? -%>
|
|
167
|
+
<a href="<%= view.h(view.base_path) %>/sessions/<%= view.h(occ["session_id"]) %>/windows/<%= view.h(occ["window_id"]) %>?t=<%= (occ["timestamp"].to_f * 1000).to_i %>" class="text-blue-500 hover:text-blue-700 font-mono"><%= view.h(occ["session_id"]) %></a>
|
|
168
|
+
<% else -%>
|
|
169
|
+
<a href="<%= view.h(view.base_path) %>/sessions/<%= view.h(occ["session_id"]) %>" class="text-blue-500 hover:text-blue-700 font-mono"><%= view.h(occ["session_id"]) %></a>
|
|
170
|
+
<% end -%>
|
|
171
|
+
</div>
|
|
172
|
+
<% end -%>
|
|
173
|
+
<% if occ["backtrace"] && !occ["backtrace"].empty? -%>
|
|
174
|
+
<pre class="text-[10px] text-gray-500 bg-gray-50 rounded p-2 overflow-x-auto whitespace-pre-wrap"><%= view.h(Array(occ["backtrace"]).first(20).join("\n")) %></pre>
|
|
175
|
+
<% end -%>
|
|
176
|
+
<% ctx = occ["context"]
|
|
177
|
+
if ctx.is_a?(Hash) && !ctx.empty? -%>
|
|
178
|
+
<details class="mt-2">
|
|
179
|
+
<summary class="text-[10px] font-medium text-gray-500 cursor-pointer select-none">Context</summary>
|
|
180
|
+
<div class="grid grid-cols-[140px_1fr] gap-x-3 gap-y-0.5 mt-1.5 bg-gray-50 rounded p-2">
|
|
181
|
+
<% ctx.each do |k, v| -%>
|
|
182
|
+
<% if v.is_a?(Hash) -%>
|
|
183
|
+
<% v.each do |sk, sv| -%>
|
|
184
|
+
<div class="text-[10px] text-gray-400 font-mono pt-0.5 truncate"><%= view.h("#{k}.#{sk}") %></div>
|
|
185
|
+
<div class="text-[10px] text-gray-700 break-all"><%= view.h(sv.is_a?(Hash) || sv.is_a?(Array) ? sv.to_json : sv.to_s) %></div>
|
|
186
|
+
<% end -%>
|
|
187
|
+
<% else -%>
|
|
188
|
+
<div class="text-[10px] text-gray-400 font-mono pt-0.5 truncate"><%= view.h(k.to_s) %></div>
|
|
189
|
+
<div class="text-[10px] text-gray-700 break-all"><%= view.h(v.is_a?(Hash) || v.is_a?(Array) ? v.to_json : v.to_s) %></div>
|
|
190
|
+
<% end -%>
|
|
191
|
+
<% end -%>
|
|
192
|
+
</div>
|
|
193
|
+
</details>
|
|
194
|
+
<% end -%>
|
|
195
|
+
</div>
|
|
196
|
+
<% end -%>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
<% end -%>
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
<%= view.render_partial("_analytics_nav.html.erb", active: :segments, since: view.filters[:since_param].to_s, until_str: view.filters[:until_param].to_s) %>
|
|
2
|
+
<h1 class="page-title">Segments</h1>
|
|
3
|
+
|
|
4
|
+
<form method="get" action="<%= view.h(view.base_path) %>/analytics/segments">
|
|
5
|
+
<div class="card mb-4">
|
|
6
|
+
<div class="card-body">
|
|
7
|
+
<div class="flex items-end gap-3 flex-wrap">
|
|
8
|
+
<div class="shrink-0">
|
|
9
|
+
<label for="browser" class="label">Browser</label>
|
|
10
|
+
<select name="browser" id="browser" class="select" style="width:8rem">
|
|
11
|
+
<option value="">Any</option>
|
|
12
|
+
<% view.browser_options.each do |b| -%>
|
|
13
|
+
<option value="<%= view.h(b) %>" <%= view.filters[:browser] == b ? "selected" : "" %>><%= view.h(b) %></option>
|
|
14
|
+
<% end -%>
|
|
15
|
+
</select>
|
|
16
|
+
</div>
|
|
17
|
+
<div class="shrink-0">
|
|
18
|
+
<label for="device" class="label">Device</label>
|
|
19
|
+
<select name="device" id="device" class="select" style="width:8rem">
|
|
20
|
+
<option value="">Any</option>
|
|
21
|
+
<% view.device_options.each do |d| -%>
|
|
22
|
+
<option value="<%= view.h(d) %>" <%= view.filters[:device] == d ? "selected" : "" %>><%= view.h(d) %></option>
|
|
23
|
+
<% end -%>
|
|
24
|
+
</select>
|
|
25
|
+
</div>
|
|
26
|
+
<div class="shrink-0">
|
|
27
|
+
<label for="url_pattern" class="label">URL pattern</label>
|
|
28
|
+
<input type="text" name="url_pattern" id="url_pattern" class="input" style="width:12rem" value="<%= view.h(view.filters[:url_pattern].to_s) %>" placeholder="substring or *glob*">
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div class="flex items-end gap-3 flex-wrap mt-3">
|
|
33
|
+
<div class="shrink-0">
|
|
34
|
+
<label for="metadata_key" class="label">Metadata key</label>
|
|
35
|
+
<input type="text" name="metadata_key" id="metadata_key" class="input" style="width:9rem" value="<%= view.h(view.filters[:metadata_key].to_s) %>">
|
|
36
|
+
</div>
|
|
37
|
+
<div class="shrink-0">
|
|
38
|
+
<label for="metadata_value" class="label">Metadata value</label>
|
|
39
|
+
<input type="text" name="metadata_value" id="metadata_value" class="input" style="width:9rem" value="<%= view.h(view.filters[:metadata_value].to_s) %>">
|
|
40
|
+
</div>
|
|
41
|
+
<div class="shrink-0">
|
|
42
|
+
<label for="metadata_match" class="label">Match</label>
|
|
43
|
+
<select name="metadata_match" id="metadata_match" class="select" style="width:7rem">
|
|
44
|
+
<option value="exact" <%= view.filters[:metadata_match] == "exact" ? "selected" : "" %>>exact</option>
|
|
45
|
+
<option value="contains" <%= view.filters[:metadata_match] == "contains" ? "selected" : "" %>>contains</option>
|
|
46
|
+
</select>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<div class="flex items-end gap-3 flex-wrap mt-3">
|
|
51
|
+
<%= view.render_partial("_date_range.html.erb",
|
|
52
|
+
since: view.filters[:since_param].to_s, until_str: view.filters[:until_param].to_s) %>
|
|
53
|
+
<div class="shrink-0">
|
|
54
|
+
<label for="min_duration" class="label">Min duration (s)</label>
|
|
55
|
+
<input type="number" min="0" name="min_duration" id="min_duration" class="input" style="width:8rem" value="<%= view.filters[:min_duration_ms] ? view.filters[:min_duration_ms] / 1000 : "" %>">
|
|
56
|
+
</div>
|
|
57
|
+
<div class="shrink-0">
|
|
58
|
+
<label for="max_duration" class="label">Max duration (s)</label>
|
|
59
|
+
<input type="number" min="0" name="max_duration" id="max_duration" class="input" style="width:8rem" value="<%= view.filters[:max_duration_ms] ? view.filters[:max_duration_ms] / 1000 : "" %>">
|
|
60
|
+
</div>
|
|
61
|
+
<div class="shrink-0 flex items-center gap-1.5 self-end pb-1.5">
|
|
62
|
+
<input type="checkbox" name="has_errors" id="has_errors" value="true" class="checkbox" <%= view.filters[:has_errors] ? "checked" : "" %>>
|
|
63
|
+
<label for="has_errors" class="label mb-0">Has errors</label>
|
|
64
|
+
</div>
|
|
65
|
+
<div class="shrink-0 flex items-center gap-1.5">
|
|
66
|
+
<button type="submit" class="btn btn-primary">Filter</button>
|
|
67
|
+
<a href="<%= view.h(view.base_path) %>/analytics/segments" class="btn btn-secondary">Clear</a>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</form>
|
|
73
|
+
|
|
74
|
+
<%= view.render_partial("_truncation_warning.html.erb", was_truncated: view.was_truncated, noun: "matching sessions") %>
|
|
75
|
+
|
|
76
|
+
<% if view.sessions.empty? -%>
|
|
77
|
+
<div class="card">
|
|
78
|
+
<div class="text-center py-10 px-6 text-gray-400 text-xs">
|
|
79
|
+
No sessions matched this filter.
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
<% else -%>
|
|
83
|
+
<div class="table-wrap">
|
|
84
|
+
<table class="data-table">
|
|
85
|
+
<thead>
|
|
86
|
+
<tr>
|
|
87
|
+
<th>Session ID</th>
|
|
88
|
+
<th>Page</th>
|
|
89
|
+
<th>Device</th>
|
|
90
|
+
<th>Windows</th>
|
|
91
|
+
<th>Events</th>
|
|
92
|
+
<th>Duration</th>
|
|
93
|
+
<th>Last Activity</th>
|
|
94
|
+
</tr>
|
|
95
|
+
</thead>
|
|
96
|
+
<tbody>
|
|
97
|
+
<% view.sessions.each do |s| -%>
|
|
98
|
+
<%= view.render_session_row(s, false) %>
|
|
99
|
+
<% end -%>
|
|
100
|
+
</tbody>
|
|
101
|
+
</table>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<nav aria-label="Segment pagination" class="mt-3">
|
|
105
|
+
<ul class="pagination">
|
|
106
|
+
<li class="<%= 'page-disabled' if view.page <= 1 %>">
|
|
107
|
+
<a class="page-link" href="<%= view.h(view.base_path) %>/analytics/segments?<%= view.h(view.filter_query) %>&per_page=<%= view.per_page %>&page=<%= view.page - 1 %>">Previous</a>
|
|
108
|
+
</li>
|
|
109
|
+
<li class="<%= 'page-disabled' unless view.has_next %>">
|
|
110
|
+
<a class="page-link" href="<%= view.h(view.base_path) %>/analytics/segments?<%= view.h(view.filter_query) %>&per_page=<%= view.per_page %>&page=<%= view.page + 1 %>">Next</a>
|
|
111
|
+
</li>
|
|
112
|
+
</ul>
|
|
113
|
+
</nav>
|
|
114
|
+
<% end -%>
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
<div class="card mb-3">
|
|
2
|
+
<div class="flex items-center gap-2 px-4 py-2.5">
|
|
3
|
+
<a href="<%= view.h(view.base_path) %>/" class="btn btn-sm btn-secondary shrink-0">← Back</a>
|
|
4
|
+
<div class="w-px h-4 bg-gray-200 shrink-0"></div>
|
|
5
|
+
<% if view.meta && view.meta["userAgent"] -%>
|
|
6
|
+
<span class="badge badge-neutral shrink-0"><%= view.h(view.parse_browser(view.meta["userAgent"]).to_s) %></span>
|
|
7
|
+
<span class="badge badge-neutral shrink-0"><%= view.h(view.parse_device(view.meta["userAgent"]).to_s) %></span>
|
|
8
|
+
<% end -%>
|
|
9
|
+
<span id="viewport-info" class="badge badge-info shrink-0" style="display:none"></span>
|
|
10
|
+
<span id="scroll-depth-info" class="badge badge-info shrink-0" style="display:none"></span>
|
|
11
|
+
<span class="badge badge-neutral shrink-0"><%= view.h(view.duration_str) %></span>
|
|
12
|
+
<span class="badge badge-neutral shrink-0"><%= view.total_events %> events</span>
|
|
13
|
+
<span id="web-vitals-badges" class="items-center gap-2 shrink-0" style="display:none"></span>
|
|
14
|
+
<div class="flex-1"></div>
|
|
15
|
+
<button id="toggle-clicks-btn" class="btn btn-sm btn-secondary shrink-0" data-action="toggle-clicks" aria-pressed="false">Clicks</button>
|
|
16
|
+
<button id="copy-link-btn" class="btn btn-sm btn-secondary shrink-0" data-action="copy-link">Copy Link</button>
|
|
17
|
+
<button class="btn btn-sm btn-secondary shrink-0" data-action="download-json">Download JSON</button>
|
|
18
|
+
<% if view.shareable_replays -%>
|
|
19
|
+
<a class="btn btn-sm btn-secondary shrink-0" href="<%= view.h(view.base_path) %>/analytics/share/<%= view.h(view.session_id) %>">Download HTML</a>
|
|
20
|
+
<% end -%>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<% if view.multi_window? -%>
|
|
25
|
+
<div class="window-switcher">
|
|
26
|
+
<span class="window-switcher-label">Tabs</span>
|
|
27
|
+
<div class="flex items-center gap-1 flex-wrap">
|
|
28
|
+
<% view.tabs[:visible].each do |tab| -%>
|
|
29
|
+
<%= view.render_partial("_window_tab.html.erb", session_id: view.session_id,
|
|
30
|
+
window: tab[:window], tab_num: tab[:tab_num],
|
|
31
|
+
css_class: (tab[:window][:window_id] == view.window_id) ? "btn btn-sm btn-active" : "btn btn-sm btn-secondary") %>
|
|
32
|
+
<% end -%>
|
|
33
|
+
<% unless view.tabs[:overflow].empty? -%>
|
|
34
|
+
<details class="window-overflow">
|
|
35
|
+
<summary class="btn btn-sm btn-secondary">+<%= view.tabs[:overflow].size %> more</summary>
|
|
36
|
+
<div class="window-overflow-menu">
|
|
37
|
+
<% view.tabs[:overflow].each do |tab| -%>
|
|
38
|
+
<%= view.render_partial("_window_tab.html.erb", session_id: view.session_id,
|
|
39
|
+
window: tab[:window], tab_num: tab[:tab_num],
|
|
40
|
+
css_class: (tab[:window][:window_id] == view.window_id) ? "active" : "") %>
|
|
41
|
+
<% end -%>
|
|
42
|
+
</div>
|
|
43
|
+
</details>
|
|
44
|
+
<% end -%>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
<% end -%>
|
|
48
|
+
|
|
49
|
+
<div class="grid grid-cols-1 lg:grid-cols-[1fr_280px] gap-3">
|
|
50
|
+
<div>
|
|
51
|
+
<div class="player-frame">
|
|
52
|
+
<div id="event-markers" style="display:none"></div>
|
|
53
|
+
<div id="replayer"></div>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
<div>
|
|
57
|
+
<div id="activity-sidebar" style="display:none">
|
|
58
|
+
<h6 class="text-xs font-semibold text-gray-700 mb-2 flex items-center gap-1.5">Activity <span id="activity-count" class="badge badge-neutral"></span></h6>
|
|
59
|
+
<div id="activity-list"></div>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<div class="card mt-4">
|
|
65
|
+
<div class="card-body">
|
|
66
|
+
<h6 class="text-xs font-semibold text-gray-800 mb-3 uppercase tracking-wider">Session Details</h6>
|
|
67
|
+
<div class="grid grid-cols-[100px_1fr] gap-x-4 gap-y-2">
|
|
68
|
+
<div class="text-[10px] font-medium text-gray-400 uppercase tracking-wider pt-0.5">Session ID</div>
|
|
69
|
+
<div class="session-id"><%= view.h(view.session_id) %></div>
|
|
70
|
+
|
|
71
|
+
<div class="text-[10px] font-medium text-gray-400 uppercase tracking-wider pt-0.5">Duration</div>
|
|
72
|
+
<div class="text-xs text-gray-600"><%= view.h(view.duration_str) %> <span id="active-time" class="text-gray-400" style="display:none"></span></div>
|
|
73
|
+
|
|
74
|
+
<div class="text-[10px] font-medium text-gray-400 uppercase tracking-wider pt-0.5">Started</div>
|
|
75
|
+
<div class="text-xs text-gray-600"><%= view.session[:created_at] ? Time.at(view.session[:created_at]).utc.strftime("%b %d, %Y %H:%M:%S UTC") : "N/A" %></div>
|
|
76
|
+
|
|
77
|
+
<div class="text-[10px] font-medium text-gray-400 uppercase tracking-wider pt-0.5">Tabs</div>
|
|
78
|
+
<div class="text-xs text-gray-600"><%= view.session[:windows]&.size || 0 %></div>
|
|
79
|
+
|
|
80
|
+
<div class="text-[10px] font-medium text-gray-400 uppercase tracking-wider pt-0.5">Total Events</div>
|
|
81
|
+
<div class="text-xs text-gray-600"><%= view.total_events %></div>
|
|
82
|
+
|
|
83
|
+
<% if view.multi_window? -%>
|
|
84
|
+
<div class="text-[10px] font-medium text-gray-400 uppercase tracking-wider pt-0.5">Per tab</div>
|
|
85
|
+
<div class="text-xs text-gray-600">
|
|
86
|
+
<% view.tabs[:all].each_with_index do |tab, i| -%>
|
|
87
|
+
Tab <%= tab[:tab_num] %>: <%= tab[:window][:event_count] %> events<%= i < view.tabs[:all].size - 1 ? ", " : "" %>
|
|
88
|
+
<% end -%>
|
|
89
|
+
</div>
|
|
90
|
+
<% end -%>
|
|
91
|
+
|
|
92
|
+
<% if view.meta -%>
|
|
93
|
+
<% if view.meta["url"] -%>
|
|
94
|
+
<div class="text-[10px] font-medium text-gray-400 uppercase tracking-wider pt-0.5">Page URL</div>
|
|
95
|
+
<div class="text-xs text-gray-600 break-all"><%= view.h(view.meta["url"]) %></div>
|
|
96
|
+
<% end -%>
|
|
97
|
+
|
|
98
|
+
<% if view.meta["referrer"] && !view.meta["referrer"].empty? -%>
|
|
99
|
+
<div class="text-[10px] font-medium text-gray-400 uppercase tracking-wider pt-0.5">Referrer</div>
|
|
100
|
+
<div class="text-xs text-gray-600 break-all"><%= view.h(view.meta["referrer"]) %></div>
|
|
101
|
+
<% end -%>
|
|
102
|
+
|
|
103
|
+
<% if view.meta["userAgent"] -%>
|
|
104
|
+
<div class="text-[10px] font-medium text-gray-400 uppercase tracking-wider pt-0.5">Browser</div>
|
|
105
|
+
<div class="text-xs text-gray-600"><%= view.h(view.parse_browser(view.meta["userAgent"]).to_s) %> (<%= view.h(view.parse_device(view.meta["userAgent"]).to_s) %>)</div>
|
|
106
|
+
<% end -%>
|
|
107
|
+
|
|
108
|
+
<% if view.meta["viewport"] -%>
|
|
109
|
+
<div class="text-[10px] font-medium text-gray-400 uppercase tracking-wider pt-0.5">Viewport</div>
|
|
110
|
+
<div class="text-xs text-gray-600"><%= view.h(view.meta["viewport"]) %></div>
|
|
111
|
+
<% end -%>
|
|
112
|
+
|
|
113
|
+
<% unless view.custom_keys.empty? -%>
|
|
114
|
+
<% view.custom_keys.each do |key| -%>
|
|
115
|
+
<div class="text-[10px] font-medium text-gray-400 uppercase tracking-wider pt-0.5"><%= view.h(key) %></div>
|
|
116
|
+
<div class="text-xs text-gray-600"><%= view.h(view.meta[key].to_s) %></div>
|
|
117
|
+
<% end -%>
|
|
118
|
+
<% end -%>
|
|
119
|
+
<% end -%>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<hr class="my-4 border-gray-100">
|
|
123
|
+
<form method="post" action="<%= view.h(view.base_path) %>/sessions/<%= view.h(view.session_id) %>/windows/<%= view.h(view.window_id) %>?_method=delete" class="inline">
|
|
124
|
+
<input type="hidden" name="csrf_token" value="<%= view.h(view.csrf_token) %>">
|
|
125
|
+
<button type="submit" class="btn btn-sm btn-ghost-danger" data-action="confirm-delete" data-confirm="<%= view.multi_window? ? "Delete this tab recording? This cannot be undone." : "Delete this recording and its session? This cannot be undone." %>">
|
|
126
|
+
<%= view.multi_window? ? "Delete this tab" : "Delete recording" %>
|
|
127
|
+
</button>
|
|
128
|
+
</form>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<% if view.server_activity && !view.server_activity.empty? -%>
|
|
133
|
+
<div id="server-activity" class="card mt-4">
|
|
134
|
+
<div class="card-body">
|
|
135
|
+
<div class="flex items-center gap-3 mb-3 flex-wrap">
|
|
136
|
+
<h6 class="text-xs font-semibold text-gray-800 uppercase tracking-wider">Server Activity</h6>
|
|
137
|
+
<div class="flex items-center gap-1" data-activity-filter-type>
|
|
138
|
+
<button type="button" class="btn btn-sm btn-active" data-filter-type="all">All</button>
|
|
139
|
+
<button type="button" class="btn btn-sm btn-secondary" data-filter-type="exception">Exceptions</button>
|
|
140
|
+
<button type="button" class="btn btn-sm btn-secondary" data-filter-type="event">Events</button>
|
|
141
|
+
</div>
|
|
142
|
+
<select class="select" style="width:7rem" data-activity-filter-level>
|
|
143
|
+
<option value="">All levels</option>
|
|
144
|
+
<option value="info">info</option>
|
|
145
|
+
<option value="warn">warn</option>
|
|
146
|
+
<option value="error">error</option>
|
|
147
|
+
<option value="debug">debug</option>
|
|
148
|
+
</select>
|
|
149
|
+
</div>
|
|
150
|
+
<% view.server_activity.each do |item| -%>
|
|
151
|
+
<% if item[:kind] == "exception" -%>
|
|
152
|
+
<%- occ = item[:occurrence] -%>
|
|
153
|
+
<div class="flex items-start gap-2 py-1.5 border-l-2 border-red-400 bg-red-50 rounded mb-1 px-2" data-activity-row data-activity-kind="exception">
|
|
154
|
+
<span class="text-[10px] text-gray-400 shrink-0 tabular-nums font-mono pt-0.5">
|
|
155
|
+
<%= item[:timestamp] > 0 ? Time.at(item[:timestamp]).utc.strftime("%H:%M:%S") : "" %>
|
|
156
|
+
</span>
|
|
157
|
+
<span class="badge badge-danger shrink-0 text-[10px]">EXCEPTION</span>
|
|
158
|
+
<span class="badge badge-neutral shrink-0 text-[10px]"><%= view.h(occ["exception_class"].to_s) %></span>
|
|
159
|
+
<a href="<%= view.h(view.base_path) %>/issues/<%= view.h(occ["fingerprint"].to_s) %>" class="text-xs text-red-700 hover:text-red-900 flex-1 truncate">
|
|
160
|
+
<%= view.h(occ["message"].to_s[0, 100]) %>
|
|
161
|
+
</a>
|
|
162
|
+
</div>
|
|
163
|
+
<% else -%>
|
|
164
|
+
<%- event = item[:event] -%>
|
|
165
|
+
<div class="flex items-start gap-2 py-1.5 border-l-2 border-indigo-400 bg-indigo-50 rounded mb-1 px-2" data-activity-row data-activity-kind="event" data-activity-level="<%= view.h(event["level"].to_s) %>">
|
|
166
|
+
<span class="text-[10px] text-gray-400 shrink-0 tabular-nums font-mono pt-0.5">
|
|
167
|
+
<%= item[:timestamp] > 0 ? Time.at(item[:timestamp]).utc.strftime("%H:%M:%S") : "" %>
|
|
168
|
+
</span>
|
|
169
|
+
<span class="badge badge-info shrink-0 text-[10px]">EVENT</span>
|
|
170
|
+
<span class="badge badge-neutral shrink-0 text-[10px]"><%= view.h(event["level"].to_s) %></span>
|
|
171
|
+
<span class="text-xs text-indigo-700 flex-1">
|
|
172
|
+
<% if event["id"] -%>
|
|
173
|
+
<a href="<%= view.h(view.base_path) %>/custom-events/<%= view.h(event["id"].to_s) %>" class="text-indigo-700 hover:text-indigo-900 hover:underline"><%= view.h(event["name"].to_s) %></a>
|
|
174
|
+
<% else -%>
|
|
175
|
+
<%= view.h(event["name"].to_s) %>
|
|
176
|
+
<% end -%>
|
|
177
|
+
<% if event["payload"].is_a?(Hash) && !event["payload"].empty? -%>
|
|
178
|
+
<details class="inline-block align-middle ml-1">
|
|
179
|
+
<summary class="text-[10px] text-indigo-400 cursor-pointer select-none">payload</summary>
|
|
180
|
+
<div class="text-[10px] text-gray-600 bg-indigo-50 rounded p-1 mt-0.5 font-mono break-all"><%= view.h(event["payload"].to_json) %></div>
|
|
181
|
+
</details>
|
|
182
|
+
<% end -%>
|
|
183
|
+
</span>
|
|
184
|
+
</div>
|
|
185
|
+
<% end -%>
|
|
186
|
+
<% end -%>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
<% end -%>
|
|
190
|
+
|
|
191
|
+
<script type="application/json" id="sentiero-player-config"><%= view.escape_json(JSON.generate({eventsUrl: "#{view.base_path}/api/sessions/#{view.session_id}/windows/#{view.window_id}/events"})) %></script>
|
|
192
|
+
<script type="application/json" id="server-activity-markers"><%= view.escape_json(JSON.generate(view.server_markers || [])) %></script>
|
|
193
|
+
<link rel="stylesheet" href="<%= view.built_asset('rrweb-player-css') %>">
|
|
194
|
+
<script src="<%= view.built_asset('rrweb-player') %>"></script>
|
|
195
|
+
<script src="<%= view.built_asset('dashboard') %>"></script>
|