sentiero 1.0.0.beta3 → 1.0.0.beta4
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 +4 -4
- data/lib/sentiero/analytics/error_discovery.rb +33 -3
- data/lib/sentiero/analytics/exporter.rb +1 -1
- data/lib/sentiero/store/error_store.rb +28 -0
- data/lib/sentiero/stores/file.rb +47 -0
- data/lib/sentiero/stores/memory.rb +27 -1
- data/lib/sentiero/stores/redis/keys.rb +4 -0
- data/lib/sentiero/stores/redis.rb +29 -0
- data/lib/sentiero/stores/sqlite/schema.rb +7 -0
- data/lib/sentiero/stores/sqlite.rb +52 -1
- data/lib/sentiero/version.rb +1 -1
- data/lib/sentiero/web/assets/dashboard-R7JYFY62.js +3 -0
- data/lib/sentiero/web/assets/manifest.json +2 -2
- data/lib/sentiero/web/assets/{recorder-W2RCU5VM.js → recorder-DCSAOXX4.js} +3 -3
- data/lib/sentiero/web/monitoring_app.rb +39 -4
- data/lib/sentiero/web/templates/_client_error_status_badge.html.erb +10 -0
- data/lib/sentiero/web/templates/_client_error_status_form.html.erb +12 -0
- data/lib/sentiero/web/templates/_errors_client_filter.html.erb +9 -0
- data/lib/sentiero/web/templates/client_error_show.html.erb +18 -0
- data/lib/sentiero/web/templates/errors_index.html.erb +13 -4
- data/lib/sentiero/web/views/client_error_show_view.rb +7 -0
- metadata +5 -3
- data/lib/sentiero/web/assets/dashboard-YINQE5ZH.js +0 -3
|
@@ -32,6 +32,9 @@ module Sentiero
|
|
|
32
32
|
get_only(method) || guard(event_id) || handle_event_show(env, event_id)
|
|
33
33
|
when "/issues"
|
|
34
34
|
handle_errors_index(env)
|
|
35
|
+
when %r{\A/issues/client/([^/]+)/status\z}
|
|
36
|
+
id = $1
|
|
37
|
+
post_only(method) || guard(id) || handle_client_error_status(env, id)
|
|
35
38
|
when %r{\A/issues/client/([^/]+)\z}
|
|
36
39
|
# Matched BEFORE the generic /issues/:id case so "client" isn't taken as
|
|
37
40
|
# a server fingerprint. Client error ids are ErrorDiscovery group digests,
|
|
@@ -212,6 +215,7 @@ module Sentiero
|
|
|
212
215
|
|
|
213
216
|
def handle_client_errors_index(env, params)
|
|
214
217
|
sort_by = %w[count recency].include?(params["sort_by"]) ? params["sort_by"] : "count"
|
|
218
|
+
status = Sentiero::Analytics::ErrorDiscovery::STATUS_FILTERS.include?(params["status"]) ? params["status"] : nil
|
|
215
219
|
search = params["search"]
|
|
216
220
|
since, until_time = parse_range_params(params)
|
|
217
221
|
since_param, until_param = echo_range_params(params, since, until_time)
|
|
@@ -219,7 +223,7 @@ module Sentiero
|
|
|
219
223
|
page, per_page, offset = paginate(params, default: 50, max: 200)
|
|
220
224
|
|
|
221
225
|
result = Sentiero::Analytics::ErrorDiscovery.new(Sentiero.store)
|
|
222
|
-
.grouped_errors(sort_by: sort_by, since: since, until_time: until_time)
|
|
226
|
+
.grouped_errors(sort_by: sort_by, since: since, until_time: until_time, status: status)
|
|
223
227
|
groups = result[:groups]
|
|
224
228
|
if search && !search.empty?
|
|
225
229
|
term = search.downcase
|
|
@@ -230,7 +234,7 @@ module Sentiero
|
|
|
230
234
|
audit!(env, action: :list_problems)
|
|
231
235
|
|
|
232
236
|
# Bounded count of the sibling server-exceptions tab for the empty-state cross-link.
|
|
233
|
-
sibling = if groups.empty? && search.to_s.empty? && since.nil? && until_time.nil?
|
|
237
|
+
sibling = if groups.empty? && status.nil? && search.to_s.empty? && since.nil? && until_time.nil?
|
|
234
238
|
problems = Sentiero.store.list_problems(project: nil, limit: SIBLING_COUNT_LIMIT)
|
|
235
239
|
{count: problems.size, capped: problems.size >= SIBLING_COUNT_LIMIT}
|
|
236
240
|
end
|
|
@@ -239,6 +243,7 @@ module Sentiero
|
|
|
239
243
|
source: "client",
|
|
240
244
|
groups: page_groups,
|
|
241
245
|
sibling: sibling,
|
|
246
|
+
status: status || "",
|
|
242
247
|
sort_by: sort_by,
|
|
243
248
|
search: search || "",
|
|
244
249
|
since_param: since_param,
|
|
@@ -253,13 +258,43 @@ module Sentiero
|
|
|
253
258
|
# Client-side JS error detail page. Re-runs ErrorDiscovery and finds the
|
|
254
259
|
# group whose stable :id matches.
|
|
255
260
|
def handle_client_error_show(env, id)
|
|
256
|
-
result = Sentiero::Analytics::ErrorDiscovery.new(Sentiero.store).grouped_errors
|
|
261
|
+
result = Sentiero::Analytics::ErrorDiscovery.new(Sentiero.store).grouped_errors(status: "all")
|
|
257
262
|
group = result[:groups].find { |g| g[:id] == id }
|
|
258
263
|
return not_found if group.nil?
|
|
259
264
|
|
|
260
265
|
audit!(env, action: :view_client_error)
|
|
261
266
|
|
|
262
|
-
render_page(env, Views::ClientErrorShowView.new(group: group, was_truncated: result[:was_truncated])
|
|
267
|
+
render_page(env, Views::ClientErrorShowView.new(group: group, was_truncated: result[:was_truncated]))
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def handle_client_error_status(env, id)
|
|
271
|
+
request = Rack::Request.new(env)
|
|
272
|
+
return forbidden_csrf unless valid_csrf_token?(env, request.POST["csrf_token"])
|
|
273
|
+
|
|
274
|
+
status = request.POST["status"]
|
|
275
|
+
return [400, {"content-type" => "text/plain"}, ["bad status"]] unless %w[open resolved ignored].include?(status)
|
|
276
|
+
|
|
277
|
+
group = Sentiero::Analytics::ErrorDiscovery.new(Sentiero.store).grouped_errors(status: "all")[:groups].find { |g| g[:id] == id }
|
|
278
|
+
return not_found if group.nil?
|
|
279
|
+
|
|
280
|
+
Sentiero.store.set_client_error_state(id, status: status, changed_at: Time.now.to_f, last_seen_at: group[:last_seen_at])
|
|
281
|
+
audit!(env, action: :update_client_error_status)
|
|
282
|
+
|
|
283
|
+
redirect(issues_referer(env) || "#{base_path(env)}/issues/client/#{id}", status: 303)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def issues_referer(env)
|
|
287
|
+
referer = env["HTTP_REFERER"]
|
|
288
|
+
return nil if referer.nil? || referer.empty?
|
|
289
|
+
|
|
290
|
+
uri = URI.parse(referer)
|
|
291
|
+
request = Rack::Request.new(env)
|
|
292
|
+
return nil if uri.host && uri.host != request.host
|
|
293
|
+
return nil unless uri.path == "#{base_path(env)}/issues"
|
|
294
|
+
|
|
295
|
+
uri.query ? "#{uri.path}?#{uri.query}" : uri.path
|
|
296
|
+
rescue URI::InvalidURIError
|
|
297
|
+
nil
|
|
263
298
|
end
|
|
264
299
|
|
|
265
300
|
def handle_error_show(env, problem_id)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<% case status
|
|
2
|
+
when "resolved" -%>
|
|
3
|
+
<span class="badge badge-success">resolved</span>
|
|
4
|
+
<% when "ignored" -%>
|
|
5
|
+
<span class="badge badge-neutral">ignored</span>
|
|
6
|
+
<% when "regressed" -%>
|
|
7
|
+
<span class="badge badge-warning">regressed</span>
|
|
8
|
+
<% else -%>
|
|
9
|
+
<span class="badge badge-danger">open</span>
|
|
10
|
+
<% end -%>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<form method="post" action="<%= view.h(view.base_path) %>/issues/client/<%= view.h(group[:id]) %>/status" class="inline-flex gap-1">
|
|
2
|
+
<input type="hidden" name="csrf_token" value="<%= view.h(view.csrf_token) %>">
|
|
3
|
+
<% unless group[:status] == "resolved" -%>
|
|
4
|
+
<button type="submit" name="status" value="resolved" class="btn btn-sm btn-primary">Resolve</button>
|
|
5
|
+
<% end -%>
|
|
6
|
+
<% unless group[:status] == "ignored" -%>
|
|
7
|
+
<button type="submit" name="status" value="ignored" class="btn btn-sm btn-secondary">Ignore</button>
|
|
8
|
+
<% end -%>
|
|
9
|
+
<% unless group[:status] == "open" -%>
|
|
10
|
+
<button type="submit" name="status" value="open" class="btn btn-sm btn-secondary">Reopen</button>
|
|
11
|
+
<% end -%>
|
|
12
|
+
</form>
|
|
@@ -7,6 +7,15 @@
|
|
|
7
7
|
<input type="text" name="search" id="search" class="input w-56"
|
|
8
8
|
value="<%= view.h(search) %>" placeholder="Error message…">
|
|
9
9
|
</div>
|
|
10
|
+
<div class="shrink-0">
|
|
11
|
+
<label for="status" class="label">Status</label>
|
|
12
|
+
<select name="status" id="status" class="select w-36">
|
|
13
|
+
<option value="" <%= "selected" if status.empty? || status == "open" %>>Open</option>
|
|
14
|
+
<option value="resolved" <%= "selected" if status == "resolved" %>>Resolved</option>
|
|
15
|
+
<option value="ignored" <%= "selected" if status == "ignored" %>>Ignored</option>
|
|
16
|
+
<option value="all" <%= "selected" if status == "all" %>>All</option>
|
|
17
|
+
</select>
|
|
18
|
+
</div>
|
|
10
19
|
<div class="shrink-0">
|
|
11
20
|
<label for="sort_by" class="label">Sort by</label>
|
|
12
21
|
<select name="sort_by" id="sort_by" class="select w-36">
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
<a href="<%= view.h(view.base_path) %>/issues?source=client" class="btn btn-sm btn-secondary shrink-0">← Back</a>
|
|
4
4
|
<div class="w-px h-4 bg-gray-200 shrink-0"></div>
|
|
5
5
|
<span class="badge badge-danger shrink-0"><%= view.group[:count] %> occurrence<%= view.group[:count] == 1 ? "" : "s" %></span>
|
|
6
|
+
<%= view.render_partial("_client_error_status_badge.html.erb", status: view.status) %>
|
|
7
|
+
<div class="ml-auto shrink-0">
|
|
8
|
+
<%= view.render_partial("_client_error_status_form.html.erb", group: view.group) %>
|
|
9
|
+
</div>
|
|
6
10
|
</div>
|
|
7
11
|
</div>
|
|
8
12
|
|
|
@@ -24,6 +28,20 @@
|
|
|
24
28
|
<%= view.group[:last_seen_at] ? Time.at(view.group[:last_seen_at].to_f / 1000).utc.strftime("%b %d, %Y %H:%M:%S UTC") : "N/A" %>
|
|
25
29
|
</div>
|
|
26
30
|
|
|
31
|
+
<% if view.status != "open" -%>
|
|
32
|
+
<div class="text-[10px] font-medium text-gray-400 uppercase tracking-wider pt-0.5">Status</div>
|
|
33
|
+
<div class="text-xs text-gray-600" data-client-error-status="<%= view.h(view.status) %>">
|
|
34
|
+
<% case view.status
|
|
35
|
+
when "resolved" -%>
|
|
36
|
+
Resolved on <%= view.h(view.changed_on) %>
|
|
37
|
+
<% when "ignored" -%>
|
|
38
|
+
Ignored on <%= view.h(view.changed_on) %>
|
|
39
|
+
<% when "regressed" -%>
|
|
40
|
+
Resolved on <%= view.h(view.changed_on) %>, seen again <%= view.group[:regressions] %> time<%= view.group[:regressions] == 1 ? "" : "s" %> since
|
|
41
|
+
<% end -%>
|
|
42
|
+
</div>
|
|
43
|
+
<% end -%>
|
|
44
|
+
|
|
27
45
|
<% if view.group[:stack] -%>
|
|
28
46
|
<div class="text-[10px] font-medium text-gray-400 uppercase tracking-wider pt-0.5">Stack</div>
|
|
29
47
|
<pre class="text-[10px] text-gray-500 bg-gray-50 rounded p-2 overflow-x-auto whitespace-pre-wrap font-mono" data-client-error-stack><%= view.h(view.group[:stack]) %></pre>
|
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
<% if view.source == "client" -%>
|
|
8
8
|
<%= view.render_partial("_truncation_warning.html.erb", was_truncated: view.was_truncated, noun: "entries") %>
|
|
9
9
|
|
|
10
|
-
<%= view.render_partial("_errors_client_filter.html.erb", search: view.search, sort_by: view.sort_by, per_page: view.per_page,
|
|
10
|
+
<%= view.render_partial("_errors_client_filter.html.erb", search: view.search, status: view.status, sort_by: view.sort_by, per_page: view.per_page,
|
|
11
11
|
since_param: view.since_param, until_param: view.until_param) %>
|
|
12
12
|
|
|
13
13
|
<% if view.groups.empty? -%>
|
|
14
14
|
<div class="card"><div class="text-center py-10 px-6 text-gray-400 text-xs">
|
|
15
|
-
<% if !view.search.empty? -%>
|
|
15
|
+
<% if !view.search.empty? || !view.status.empty? || !view.since_param.empty? || !view.until_param.empty? -%>
|
|
16
16
|
No results match your filters.
|
|
17
17
|
<a href="<%= view.h(view.base_path) %>/issues?source=client" class="text-blue-600 hover:text-blue-800">Clear filters</a>
|
|
18
18
|
<% else -%>
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
<% else -%>
|
|
25
25
|
<div class="table-wrap">
|
|
26
26
|
<table class="data-table">
|
|
27
|
-
<thead><tr><th>Error</th><th class="w-24">Browser</th><th class="w-20">Count</th><th class="w-32">Last seen</th></tr></thead>
|
|
27
|
+
<thead><tr><th>Error</th><th class="w-24">Browser</th><th class="w-20">Count</th><th class="w-24">Status</th><th class="w-32">Last seen</th><th></th></tr></thead>
|
|
28
28
|
<tbody>
|
|
29
29
|
<% view.groups.each do |group| -%>
|
|
30
30
|
<tr>
|
|
@@ -50,9 +50,18 @@
|
|
|
50
50
|
<% end -%>
|
|
51
51
|
</td>
|
|
52
52
|
<td><span class="badge badge-danger tabular-nums"><%= group[:count] %>×</span></td>
|
|
53
|
+
<td>
|
|
54
|
+
<%= view.render_partial("_client_error_status_badge.html.erb", status: group[:status]) %>
|
|
55
|
+
<% if group[:status] == "regressed" -%>
|
|
56
|
+
<span class="block text-[10px] text-gray-400 mt-0.5 tabular-nums"><%= group[:regressions] %>× since <%= Time.at(group.dig(:state, :changed_at).to_f).utc.strftime("%b %d") %></span>
|
|
57
|
+
<% end -%>
|
|
58
|
+
</td>
|
|
53
59
|
<td class="text-gray-400 text-[10px] tabular-nums">
|
|
54
60
|
<%= group[:last_seen_at] ? Time.at(group[:last_seen_at].to_f / 1000).utc.strftime("%b %d, %H:%M") : "—" %>
|
|
55
61
|
</td>
|
|
62
|
+
<td class="text-right">
|
|
63
|
+
<%= view.render_partial("_client_error_status_form.html.erb", group: group) %>
|
|
64
|
+
</td>
|
|
56
65
|
</tr>
|
|
57
66
|
<% end -%>
|
|
58
67
|
</tbody>
|
|
@@ -61,7 +70,7 @@
|
|
|
61
70
|
|
|
62
71
|
<%= view.render_partial("_pagination.html.erb",
|
|
63
72
|
base_href: "#{view.base_path}/issues", page: view.page, has_next: view.has_next,
|
|
64
|
-
params: {"source" => "client", "search" => view.search, "sort_by" => view.sort_by,
|
|
73
|
+
params: {"source" => "client", "search" => view.search, "status" => view.status, "sort_by" => view.sort_by,
|
|
65
74
|
"since" => view.since_param, "until" => view.until_param, "per_page" => view.per_page.to_s}) %>
|
|
66
75
|
<% end -%>
|
|
67
76
|
<% else -%>
|
|
@@ -16,6 +16,13 @@ module Sentiero
|
|
|
16
16
|
|
|
17
17
|
def template = "client_error_show.html.erb"
|
|
18
18
|
|
|
19
|
+
def status = group[:status]
|
|
20
|
+
|
|
21
|
+
def changed_on
|
|
22
|
+
changed_at = group.dig(:state, :changed_at)
|
|
23
|
+
changed_at ? Time.at(changed_at.to_f).utc.strftime("%b %d, %Y %H:%M UTC") : nil
|
|
24
|
+
end
|
|
25
|
+
|
|
19
26
|
def facet_chips
|
|
20
27
|
[
|
|
21
28
|
["Browsers", group[:browsers] || {}],
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sentiero
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.0.
|
|
4
|
+
version: 1.0.0.beta4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stephen Ierodiaconou
|
|
@@ -123,11 +123,11 @@ files:
|
|
|
123
123
|
- lib/sentiero/version.rb
|
|
124
124
|
- lib/sentiero/web/analytics_app.rb
|
|
125
125
|
- lib/sentiero/web/assets/analytics-RH24EOLD.js
|
|
126
|
-
- lib/sentiero/web/assets/dashboard-
|
|
126
|
+
- lib/sentiero/web/assets/dashboard-R7JYFY62.js
|
|
127
127
|
- lib/sentiero/web/assets/heatmap-EBKFWSKN.js
|
|
128
128
|
- lib/sentiero/web/assets/import-HIMBJJ4S.js
|
|
129
129
|
- lib/sentiero/web/assets/manifest.json
|
|
130
|
-
- lib/sentiero/web/assets/recorder-
|
|
130
|
+
- lib/sentiero/web/assets/recorder-DCSAOXX4.js
|
|
131
131
|
- lib/sentiero/web/assets/rrweb-player-css-ce5e9629.css
|
|
132
132
|
- lib/sentiero/web/assets/rrweb-player-d522d74c.js
|
|
133
133
|
- lib/sentiero/web/assets/sessions_index-2RAGTEZM.js
|
|
@@ -150,6 +150,8 @@ files:
|
|
|
150
150
|
- lib/sentiero/web/shareable_replay.rb
|
|
151
151
|
- lib/sentiero/web/templates/_analytics_nav.html.erb
|
|
152
152
|
- lib/sentiero/web/templates/_brand.html.erb
|
|
153
|
+
- lib/sentiero/web/templates/_client_error_status_badge.html.erb
|
|
154
|
+
- lib/sentiero/web/templates/_client_error_status_form.html.erb
|
|
153
155
|
- lib/sentiero/web/templates/_date_range.html.erb
|
|
154
156
|
- lib/sentiero/web/templates/_errors_client_filter.html.erb
|
|
155
157
|
- lib/sentiero/web/templates/_errors_server_filter.html.erb
|
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
(()=>{var C={navigation:{label:"Navigation",color:"#198754"},click:{label:"Click",color:"#dc3545"},input:{label:"Input",color:"#0d6efd"},error:{label:"Error",color:"#ff0000"},custom:{label:"Custom",color:"#6f42c1"},frustration:{label:"Frustration",color:"#ff6b6b"},server_exception:{label:"Server Exception",color:"#b91c1c"},server_event:{label:"Server Event",color:"#7c3aed"}},k=5e3,it=3;function lt(t){if(t.length<2)return 0;let e=0,r=t[0].timestamp,n=t[0].timestamp;for(let a=1;a<t.length;a++){let o=t[a].timestamp;o-n>k&&(e+=n-r,r=o),n=o}return e+=n-r,e}function S(t){let e=Math.round(t/1e3);if(e<60)return`${e}s`;if(e<3600){let a=Math.floor(e/60),o=e%60;return o>0?`${a}m ${o}s`:`${a}m`}let r=Math.floor(e/3600),n=Math.floor(e%3600/60);return n>0?`${r}h ${n}m`:`${r}h`}function M(t){let e=Math.floor(t/1e3),r=Math.floor(e/3600),n=Math.floor(e%3600/60),a=e%60,o=i=>(i<10?"0":"")+i;return r>0?`${r}:${o(n)}:${o(a)}`:`${n}:${o(a)}`}function _(t,e){return t.length<=e?t:t.substring(0,e-3)+"..."}function ct(t){for(let e=0;e<t.length;e++)if(t[e].type===4&&t[e].data){let r=t[e].data.width,n=t[e].data.height;if(r&&n)return`${r}x${n}`}return null}function Bt(t){return!!(t&&t.type===3&&t.data&&t.data.source===5&&typeof t.data.id=="number")}function Ht(t){return!!t&&typeof t.isChecked=="boolean"}function W(t){if(!Array.isArray(t))return[];let e=new Map;for(let n=0;n<t.length;n++){let a=t[n];if(!Bt(a))continue;let o=a.data.id,i=e.get(o);i||(i={nodeId:o,fillCount:0,isToggle:!1,firstTimestamp:a.timestamp,lastTimestamp:a.timestamp},e.set(o,i)),i.fillCount+=1,i.lastTimestamp=a.timestamp,Ht(a.data)&&(i.isToggle=!0)}let r=Array.from(e.values()).sort((n,a)=>n.firstTimestamp-a.firstTimestamp);return r.forEach((n,a)=>{n.order=a+1,n.label=`Field ${a+1}`}),r}function D(t){let e=W(t),r={},n=[];for(let a=0;a<e.length;a++){let o=e[a],i=e[a+1];o.nextFieldOffset=i?i.firstTimestamp-o.firstTimestamp:null,r[o.nodeId]=o,n.push(o.label)}return{totalFields:e.length,sequence:n,byNodeId:r}}function Gt(t){return t<1e3?`${Math.round(t/100)*100}ms`:`${(Math.round(t/100)/10).toFixed(1)}s`}function st(t,e){if(!t||t.category!=="input")return[];if(!e||!e.byNodeId)return[];let r=t.event&&t.event.data;if(!r||typeof r.id!="number")return[];let n=e.byNodeId[r.id];if(!n)return[];let a=[`Field ${n.order} of ${e.totalFields}`];return n.isToggle?n.fillCount>1&&a.push(`Toggled: ${n.fillCount} times`):n.fillCount>1&&a.push(`Re-filled: ${n.fillCount} times`),n.nextFieldOffset!=null&&n.nextFieldOffset>0&&n.nextFieldOffset<=k&&a.push(`Time to next field: ${Gt(n.nextFieldOffset)}`),a}function F(t,e){let r=[],n=t;function a(){fetch(n).then(o=>{if(!o.ok)throw new Error(`Failed to fetch events: ${o.status}`);return o.json()}).then(o=>{if(!Array.isArray(o)||o.length===0){e(null,r);return}r=r.concat(o);let i=o[o.length-1];if(i&&i.timestamp){let s=t.indexOf("?")===-1?"?":"&";n=`${t}${s}after=${i.timestamp}`,a()}else e(null,r)}).catch(o=>{r.length>0?e(null,r):e(o,[])})}a()}var j=null;function w(){return j}function Vt(t){if(t.type===4&&t.data&&t.data.href)return"navigation";if(t.type===3&&t.data){if(t.data.source===2){let e=t.data.type;return e===2||e===4||e===3||e===7?"click":null}if(t.data.source===5)return"input"}return t.type===5?t.data&&t.data.tag==="error"?"error":t.data&&t.data.tag==="navigation"?"navigation":"custom":null}function Yt(t){if(!t||t.length<2)return{map:()=>0,gaps:[]};let e=t[0].timestamp,r=t[t.length-1].timestamp,n=r-e;if(n<=0)return{map:()=>0,gaps:[]};let a=[],o=[],i=e;for(let l=1;l<t.length;l++){let c=t[l].timestamp-t[l-1].timestamp;c>k&&(o.push({start:i,end:t[l-1].timestamp}),a.push({start:t[l-1].timestamp,end:t[l].timestamp,duration:c}),i=t[l].timestamp)}if(o.push({start:i,end:r}),a.length===0)return{map:l=>(l-e)/n,gaps:[]};let s=0;for(let l=0;l<o.length;l++)s+=o[l].end-o[l].start;let f=a.length*it;f>40&&(f=40);let u=f/a.length,m=100-f,p=[],d=0;for(let l=0;l<o.length;l++){let c=o[l].end-o[l].start,h=s>0?c/s*m:0;p.push({type:"active",start:o[l].start,end:o[l].end,posStart:d,posEnd:d+h}),d+=h,l<a.length&&(p.push({type:"gap",start:a[l].start,end:a[l].end,duration:a[l].duration,posStart:d,posEnd:d+u}),d+=u)}function g(l){for(let c=0;c<p.length;c++){let h=p[c];if(h.type==="active"&&l>=h.start&&l<=h.end){let v=h.end-h.start,V=v>0?(l-h.start)/v:0;return(h.posStart+V*(h.posEnd-h.posStart))/100}}for(let c=0;c<p.length;c++)if(p[c].type==="gap"&&l>=p[c].start&&l<=p[c].end)return p[c].posStart/100;return l<=e?0:1}let y=p.filter(l=>l.type==="gap");return{map:g,gaps:y}}function ut(t){if(!t||t.length===0)return[];let e=t[0].timestamp;j=Yt(t);let r=D(t),n=0,a=[];for(let o=0;o<t.length;o++){let i=Vt(t[o]);if(!i)continue;let s={index:o,timestamp:t[o].timestamp,offset:t[o].timestamp-e,position:j.map(t[o].timestamp),category:i,event:t[o]};i==="navigation"&&t[o].type===4&&(n++,s.metaIndex=n),i==="input"&&(s.formContext=r),a.push(s)}return a}function mt(t){return Array.isArray(t)?t.map(e=>({offset:e.offset_ms||0,category:e.kind==="exception"?"server_exception":"server_event",kind:e.kind,label:e.label||"",level:e.level||"",href:e.href||null,isServerMarker:!0})):[]}function q(t){return typeof t.isChecked=="boolean"}function R(t){if(t.isServerMarker)return t.label||(t.category==="server_exception"?"Server exception":"Server event");if(t.category==="navigation"&&t.event.data&&t.event.data.href){let r=t.event.data.href;try{let n=_(new URL(r).pathname,40);return t.metaIndex===1?`Page loaded: ${n}`:`Navigated to: ${n}`}catch{return t.metaIndex===1?"Page loaded":"Page navigation"}}if(t.category==="navigation"&&t.event.data&&t.event.data.payload){let r=t.event.data.payload,n=r.url||"",a=r.external?"Leaving to: ":"Navigating to: ";try{let o=new URL(n),i=r.external?o.hostname+o.pathname:o.pathname;return a+_(i,40)}catch{return a+_(n,40)}}if(t.category==="click"&&t.event.data){let r=t.event.data.type,n="Click";return r===4?n="Double Click":r===3?n="Right Click":r===7&&(n="Touch"),t.event.data.x!=null&&t.event.data.y!=null&&(n+=` (${Math.round(t.event.data.x)}, ${Math.round(t.event.data.y)})`),n}if(t.category==="input"&&t.event.data){let r=t.event.data;return typeof r.text=="string"&&r.text.length>0&&!q(r)?`Input: ${_(r.text,20)}`:q(r)?r.isChecked?"Checkbox checked":"Checkbox unchecked":"Input cleared"}if(t.category==="frustration"){let r=t.x!=null&&t.y!=null?` (${Math.round(t.x)}, ${Math.round(t.y)})`:"";return t.subtype==="rage_click"?`Rage click${r}`:`Dead click${r}`}if(t.category==="error"&&t.event.data&&t.event.data.payload){let r=t.event.data.payload.message||"Error";return _(r,50)}if(t.category==="custom"&&t.event.data&&t.event.data.tag)return t.event.data.tag;let e=C[t.category];return e?e.label:"Event"}function yt(t){if(t.isServerMarker){let n=[];return t.level&&n.push(`Level: ${t.level}`),t.href&&n.push(`Details: ${t.href}`),n}let e=[],r=t.event.data||{};if(t.category==="frustration")return t.x!=null&&t.y!=null&&e.push(`Position: (${Math.round(t.x)}, ${Math.round(t.y)})`),t.subtype==="rage_click"?e.push(`Clicks: ${t.count}`):e.push(`No response within ${t.elapsed||500}ms`),e;if(t.category==="navigation")return r.href?(e.push(`URL: ${r.href}`),r.width&&r.height&&e.push(`Viewport: ${r.width}x${r.height}`)):r.payload&&(r.payload.url&&e.push(`URL: ${r.payload.url}`),r.payload.text&&e.push(`Link text: ${r.payload.text}`),r.payload.external&&e.push("External link")),e;if(t.category==="click")r.x!=null&&r.y!=null&&e.push(`Position: (${Math.round(r.x)}, ${Math.round(r.y)})`);else if(t.category==="input")typeof r.text=="string"&&r.text.length>0&&e.push(`Value: ${r.text}`),q(r)&&e.push(`Checked: ${r.isChecked}`),st(t,t.formContext).forEach(n=>e.push(n));else if(t.category==="error"&&r.payload){let n=r.payload;if(n.message&&e.push(`Message: ${n.message}`),n.source){let a=n.source;n.lineno&&(a+=`:${n.lineno}`),n.colno&&(a+=`:${n.colno}`),e.push(`Source: ${a}`)}n.type&&e.push(`Type: ${n.type}`),n.stack&&e.push(`Stack:
|
|
2
|
-
${n.stack}`)}else if(t.category==="custom"&&r.payload)try{e.push(`Payload: ${JSON.stringify(r.payload,null,2)}`)}catch{e.push("Payload: [object]")}return e}var Wt=1.5,ht=!1,X=null,gt=0;function jt(t){if(t.length===0)return[];let e=[],r={events:[t[0]],position:t[0].position*100};for(let n=1;n<t.length;n++){let a=t[n].position*100;Math.abs(a-r.position)<=Wt?r.events.push(t[n]):(e.push(r),r={events:[t[n]],position:a})}return e.push(r),e}function J(){let t=document.querySelectorAll(".marker-dropdown");for(let e=0;e<t.length;e++)t[e].parentNode.removeChild(t[e])}function Et(t){if(!X)return;let e=w();if(!e)return;let r=e.map(gt+t)*100;X.style.left=`${Math.max(0,Math.min(100,r))}%`}function xt(t,e){let r=document.getElementById("event-markers");if(!r)return;if(t.length===0){r.style.display="none";return}e&&e.length>0&&(gt=e[0].timestamp),r.innerHTML="";let n=document.createElement("div");n.className="marker-legend",Object.keys(C).forEach(m=>{let p=C[m],d=document.createElement("span");d.className="marker-legend-item";let g=document.createElement("span");g.className="marker-legend-dot",g.style.backgroundColor=p.color,d.appendChild(g),d.appendChild(document.createTextNode(p.label)),n.appendChild(d)}),r.appendChild(n);let a=document.createElement("div");a.className="marker-bar";let o=document.createElement("div");o.className="marker-track",a.appendChild(o);let i=document.createElement("div");i.className="marker-playhead",i.style.left="0%",a.appendChild(i),X=i;let s=t;s.length>500&&(s=t.filter(m=>m.category!=="input")),jt(s).forEach(m=>{let p=`${Math.max(1,Math.min(99,m.position))}%`;if(m.events.length===1){let d=m.events[0],g=C[d.category];if(!g)return;let y=document.createElement("div");y.className=`event-marker${d.category==="error"?" error-marker":""}`,y.style.left=p,y.style.backgroundColor=g.color,y.title=`${R(d)} at ${M(d.offset)}`,y.addEventListener("click",()=>P(d.offset)),a.appendChild(y)}else{let d=m.events.some(c=>c.category==="error"),g=document.createElement("div");g.className="marker-group",g.style.left=p;let y=document.createElement("div");y.className=`event-marker marker-group-dot${d?" error-marker":""}`,y.style.backgroundColor=d?"#ff0000":"#6c757d",y.title=`${m.events.length} events at ${M(m.events[0].offset)}`;let l=document.createElement("span");l.className="marker-group-count",l.textContent=m.events.length,g.appendChild(y),g.appendChild(l),g.addEventListener("click",c=>{c.stopPropagation(),J();let h=document.createElement("div");h.className="marker-dropdown",m.events.forEach(v=>{let V=C[v.category]||{},N=document.createElement("div");N.className=`marker-dropdown-item${v.category==="error"?" error-entry":""}`;let Y=document.createElement("span");Y.className="activity-dot",Y.style.backgroundColor=V.color||"#6c757d";let ot=document.createElement("span");ot.textContent=`${M(v.offset)} ${R(v)}`,N.appendChild(Y),N.appendChild(ot),N.addEventListener("click",Ft=>{Ft.stopPropagation(),J(),P(v.offset)}),h.appendChild(N)}),g.appendChild(h)}),a.appendChild(g)}});let u=w();u&&u.gaps.length>0&&u.gaps.forEach(m=>{let p=document.createElement("div");p.className="marker-gap",p.style.left=`${m.posStart}%`,p.style.width=`${m.posEnd-m.posStart}%`,p.title=`Inactive: ${S(m.duration)}`,a.appendChild(p)}),ht||(ht=!0,document.addEventListener("click",J)),r.appendChild(a),r.style.display=""}var B=null,z=[];function Ct(t){let e=document.getElementById("activity-sidebar"),r=document.getElementById("activity-list"),n=document.getElementById("activity-count");if(!(!e||!r)){if(t.length===0){e.style.display="none";return}z=t,e.style.display="",n&&(n.textContent=t.length),r.innerHTML="",t.forEach((a,o)=>{let i=C[a.category];if(!i)return;if(o>0){let y=t[o-1].offset,l=a.offset-y;if(l>k){let c=document.createElement("div");c.className="activity-gap",c.textContent=`${S(l)} inactive`,r.appendChild(c)}}let s=document.createElement("div");s.className="activity-wrapper";let f=document.createElement("div"),u="activity-entry";a.category==="error"?u+=" error-entry":a.category==="navigation"?u+=" navigation-entry":a.category==="frustration"?u+=" frustration-entry":a.category==="server_exception"?u+=" server-exception-entry":a.category==="server_event"&&(u+=" server-event-entry"),f.className=u,f.setAttribute("data-offset",a.offset),f.setAttribute("data-index",o);let m=document.createElement("span");m.className="activity-time",m.textContent=M(a.offset);let p=document.createElement("span");p.className="activity-dot",p.style.backgroundColor=i.color;let d;a.isServerMarker&&a.href?(d=document.createElement("a"),d.href=a.href,d.className="activity-label activity-label-link",d.addEventListener("click",y=>y.stopPropagation())):(d=document.createElement("span"),d.className="activity-label"),d.textContent=R(a),f.appendChild(m),f.appendChild(p),f.appendChild(d),f.addEventListener("click",()=>P(a.offset)),s.appendChild(f);let g=yt(a);if(g.length>0){let y=document.createElement("div");y.className="activity-detail",g.forEach(l=>{let c=document.createElement("div");if(c.className="activity-detail-line",l.indexOf(`
|
|
3
|
-
`)!==-1){let h=document.createElement("pre");h.className="activity-detail-pre",h.textContent=l,c.appendChild(h)}else c.textContent=l;y.appendChild(c)}),s.appendChild(y)}r.appendChild(s)}),qt()}}function Q(){B&&(clearInterval(B),B=null)}function qt(){Q(),B=setInterval(()=>{let t=A();if(t)try{let r=t.getReplayer().getCurrentTime();Kt(r),Et(r)}catch{}},250)}function Kt(t){let e=document.querySelectorAll(".activity-wrapper");if(e.length===0)return;let r=-1;for(let n=z.length-1;n>=0;n--)if(z[n].offset<=t){r=n;break}for(let n=0;n<e.length;n++){let a=e[n].querySelector(".activity-entry");a&&(n===r?a.classList.contains("active")||(a.classList.add("active"),e[n].scrollIntoView({block:"nearest",behavior:"smooth"})):a.classList.remove("active"))}}var Jt=["LCP","CLS","INP"];function Xt(t){let e=Object.create(null);if(!Array.isArray(t))return e;for(let r of t)if(r&&r.type===5&&r.data&&r.data.tag==="__perf"&&r.data.payload){let n=r.data.payload;typeof n.metric=="string"&&typeof n.value=="number"&&(e[n.metric]={value:n.value,rating:n.rating})}return e}function zt(t,e){return t==="CLS"?e.toFixed(3):`${Math.round(e)} ms`}function Qt(t){return t==="good"?"badge-success":t==="needs-improvement"?"badge-warning":t==="poor"?"badge-danger":"badge-neutral"}function vt(t,e){if(!e)return;let r=Xt(t);e.innerHTML="";let n=!1;for(let a of Jt){let o=r[a];if(!o)continue;n=!0;let i=document.createElement("span");i.className=`badge ${Qt(o.rating)} shrink-0`,i.textContent=`${a} ${zt(a,o.value)}`,o.rating&&(i.title=o.rating),e.appendChild(i)}e.style.display=n?"inline-flex":"none"}function te(t){let e=0;if(!Array.isArray(t))return e;for(let r=0;r<t.length;r++){let n=t[r];n&&n.type===3&&n.data&&n.data.source===3&&typeof n.data.y=="number"&&n.data.y>e&&(e=n.data.y)}return e}function ee(t){if(!Array.isArray(t))return null;for(let e=0;e<t.length;e++){let r=t[e];if(r&&r.type===4&&r.data&&r.data.height)return r.data.height}return null}function Z(t){let e=Math.round(te(t));if(e<=0)return null;let r=ee(t),n=r&&r>0?Math.round((e+r)/r*10)/10:null;return{y:e,viewports:n}}function Tt(t,e){if(!e)return;let r=Z(t);if(!r){e.style.display="none",e.textContent="";return}let n=r.viewports!=null?` (~${r.viewports}\xD7 viewport)`:"";e.textContent=`Max Scroll: ${r.y}px${n}`,e.style.display="inline"}var bt=6,kt=1e3,b=!1,I=null;function re(t){let e=[];if(!Array.isArray(t))return e;for(let r=0;r<t.length;r++){let n=t[r];n&&n.type===3&&n.data&&n.data.source===2&&n.data.type===2&&typeof n.data.x=="number"&&typeof n.data.y=="number"&&e.push({x:n.data.x,y:n.data.y})}return e}function ne(t){if(!Array.isArray(t))return null;for(let e=0;e<t.length;e++){let r=t[e];if(r&&r.type===4&&r.data&&r.data.width&&r.data.height)return{width:r.data.width,height:r.data.height}}return null}function ae(t,e,r){let n=e&&e.width>0?r.width/e.width:1,a=e&&e.height>0?r.height/e.height:1,o=Math.max(0,Math.min(r.width,t.x*n)),i=Math.max(0,Math.min(r.height,t.y*a));return{x:o,y:i}}function oe(){let t=document.getElementById("replayer");return t?t.querySelector(".replayer-wrapper")||t.querySelector(".rr-player__frame")||t:null}function tt(){I&&I.parentNode&&I.parentNode.removeChild(I),I=null}function ie(){tt();let t=oe();if(!t)return;let e=U(),r=ne(e),n=re(e);if(n.length>kt){let i=Math.ceil(n.length/kt);n=n.filter((s,f)=>f%i===0)}let a={width:t.clientWidth||(r?r.width:0),height:t.clientHeight||(r?r.height:0)},o=document.createElement("div");o.className="click-overlay-container",n.forEach(i=>{let{x:s,y:f}=ae(i,r,a),u=document.createElement("div");u.className="click-dot",u.style.left=`${s-bt}px`,u.style.top=`${f-bt}px`,o.appendChild(u)}),getComputedStyle(t).position==="static"&&(t.style.position="relative"),t.appendChild(o),I=o}function St(){b=!1,tt();let t=document.getElementById("toggle-clicks-btn");t&&(t.setAttribute("aria-pressed","false"),t.classList.remove("btn-active"))}function H(){if(!A())return b;b=!b,b?ie():tt();let t=document.getElementById("toggle-clicks-btn");return t&&(t.setAttribute("aria-pressed",b?"true":"false"),t.classList.toggle("btn-active",b)),b}var Mt=500,_t=10,At=3,It=500;function Nt(t){return!!(t&&t.type===3&&t.data&&t.data.source===2&&t.data.type===2&&typeof t.data.x=="number"&&typeof t.data.y=="number")}function ce(t){return!t||!t.data?!1:t.type===4?!0:t.type===3?t.data.source===0||t.data.source===5:!1}function se(t){if(!Array.isArray(t))return[];let e=[];for(let a=0;a<t.length;a++)Nt(t[a])&&e.push(t[a]);if(e.length<At)return[];let r=[],n=0;for(let a=1;a<=e.length;a++){let o=e[a-1],i=e[a],s=e[n];if(!(i&&i.timestamp-o.timestamp<=Mt&&i.timestamp-s.timestamp<=Mt&&Math.abs(i.data.x-s.data.x)<=_t&&Math.abs(i.data.y-s.data.y)<=_t)){let u=a-n;if(u>=At){let m=[];for(let p=n;p<a;p++)m.push(e[p].timestamp);r.push({subtype:"rage_click",timestamp:s.timestamp,count:u,x:s.data.x,y:s.data.y,memberTimestamps:m,event:s})}n=a}}return r}function pe(t){if(!Array.isArray(t))return[];let e=[];for(let r=0;r<t.length;r++){if(!Nt(t[r]))continue;let n=t[r].timestamp,a=n+It,o=!1;for(let i=r+1;i<t.length;i++){let s=t[i].timestamp;if(s>a)break;if(s>n&&ce(t[i])){o=!0;break}}o||e.push({subtype:"dead_click",timestamp:n,x:t[r].data.x,y:t[r].data.y,elapsed:It,event:t[r]})}return e}function G(t){if(!Array.isArray(t)||t.length===0)return[];let e=t[0].timestamp,r=se(t),n=pe(t),a=new Set;r.forEach(i=>(i.memberTimestamps||[i.timestamp]).forEach(s=>a.add(s)));let o=[];return r.forEach(i=>o.push(i)),n.forEach(i=>{a.has(i.timestamp)||o.push(i)}),o.map(i=>({category:"frustration",subtype:i.subtype,timestamp:i.timestamp,offset:i.timestamp-e,count:i.count,elapsed:i.elapsed,x:i.x,y:i.y,event:i.event})).sort((i,s)=>i.offset-s.offset)}var de=1e4,fe=1e3;function ue(t){return t.type===3&&t.data&&t.data.source>=1&&t.data.source<=5}function me(t,e=de){if(!Array.isArray(t)||t.length===0)return[];let r=t[0].timestamp,n=[0],a=!1;for(let i of t)ue(i)&&(n.push(i.timestamp-r),a=!0);if(!a)return[];n.push(t[t.length-1].timestamp-r);let o=[];for(let i=1;i<n.length;i++)n[i]-n[i-1]>e&&o.push({start:n[i-1],end:n[i]});return o}function ye(t,e,r=fe){for(let n of t){let a=n.end-r;if(e>=n.start&&e<a)return a}return null}function Lt(t,e){let r=me(e);if(r.length===0)return;let n=!1;t.addEventListener("ui-update-player-state",a=>{n=a.payload==="playing"}),t.addEventListener("ui-update-current-time",a=>{if(!n)return;let o;try{o=t.getReplayer().config.skipInactive!==!1}catch{return}if(!o)return;let i=ye(r,a.payload);i!==null&&t.goto(i,!0)})}var x=null,Pt=[],Ot=[1,2,4,8,16],et=0,$t=!1;function A(){return x}function U(){return Pt}function he(t){let e=document.getElementById("active-time");if(!e)return;let r=lt(t);if(r>0){let n=t[t.length-1].timestamp-t[0].timestamp,a=n>0?Math.round(r/n*100):100;e.textContent=`(active: ${S(r)} / ${a}%)`,e.style.display="inline"}}function ge(t){let e=document.getElementById("viewport-info");if(!e)return;let r=ct(t);r&&(e.textContent=`Viewport: ${r}`,e.style.display="inline")}function Ee(t){if(t.length===0)return null;let r=new URLSearchParams(window.location.search).get("t");if(r!==null&&r!==""){let n=parseInt(r,10);if(!isNaN(n)){let a=n-t[0].timestamp;return a>=0?t[0].timestamp+a:null}}return null}function P(t){if(x)try{let e=x.getReplayer();e&&(e.play(t),e.pause())}catch(e){console.warn("Sentiero: seek failed",e)}}function wt(t,e){try{let r=t.getMetaData(),n=t.getCurrentTime(),a=Math.max(0,Math.min(n+e,r.totalTime));t.play(a),t.pause()}catch(r){console.warn("Sentiero: seek failed",r)}}function Rt(t){et=Math.max(0,Math.min(Ot.length-1,et+t));try{let e=x.getReplayer();e&&e.setConfig&&e.setConfig({speed:Ot[et]})}catch(e){console.warn("Sentiero: speed change failed",e)}}function xe(){$t||($t=!0,document.addEventListener("keydown",t=>{if(!x||t.target.tagName==="INPUT"||t.target.tagName==="TEXTAREA")return;let e;try{e=x.getReplayer()}catch{return}if(e)switch(t.key){case" ":t.preventDefault();try{x.$$?.ctx?.[0]?x.toggle():e.play()}catch{try{e.play()}catch{}}break;case"ArrowLeft":t.preventDefault(),wt(e,-5e3);break;case"ArrowRight":t.preventDefault(),wt(e,5e3);break;case"ArrowUp":t.preventDefault(),Rt(1);break;case"ArrowDown":t.preventDefault(),Rt(-1);break}}))}function rt(t,e,r=[]){if(!e){console.error("Sentiero: player container not found");return}F(t,(n,a)=>{if(n){console.error("Sentiero: failed to load events",n),e.innerHTML="";let c=document.createElement("div");c.className="alert alert-danger",c.textContent=`Failed to load session events: ${n.message}`,e.appendChild(c);return}if(a.length===0){e.innerHTML='<p class="text-muted">No events recorded for this window.</p>';return}Pt=a,he(a),ge(a),Tt(a,document.getElementById("scroll-depth-info")),vt(a,document.getElementById("web-vitals-badges"));let o=ut(a),i=w(),s=a[0].timestamp,f=G(a);f.forEach(c=>{c.position=i?i.map(c.timestamp):0});let u=Array.isArray(r)?r:[];u.forEach(c=>{c.position=i?i.map(s+c.offset):0});let m=o.concat(f).concat(u).sort((c,h)=>c.offset-h.offset);xt(m,a),Ct(m);let p=e.closest(".player-frame")||e.parentElement,d=p?p.clientWidth:1024,g=Math.min(2048,d),y=rrwebPlayer.default||rrwebPlayer;x=new y({target:e,props:{events:a,showController:!0,autoPlay:!1,skipInactive:!0,width:g}});let l=Ee(a);if(l)try{let c=x.getReplayer();c&&setTimeout(()=>{c.play(l-a[0].timestamp),c.pause()},100)}catch(c){console.warn("Sentiero: could not seek to timestamp",c)}Lt(x,a),St(),xe()})}function nt(){let t=A(),e=U();if(!(!t||e.length===0))try{let n=t.getReplayer().getCurrentTime(),a=new URL(window.location.href);a.searchParams.set("t",Math.round(n)),navigator.clipboard.writeText(a.toString()).then(()=>{let o=document.getElementById("copy-link-btn");if(o){let i=o.textContent;o.textContent="Copied!",setTimeout(()=>{o.textContent=i},2e3)}})}catch(r){console.warn("Sentiero: copy link failed",r)}}function at(){let t=U();if(t.length!==0)try{let e=JSON.stringify(t,null,2),r=new Blob([e],{type:"application/json"}),n=URL.createObjectURL(r),a=document.createElement("a");a.href=n,a.download="sentiero-session-events.json",document.body.appendChild(a),a.click(),document.body.removeChild(a),URL.revokeObjectURL(n)}catch(e){console.warn("Sentiero: download failed",e)}}function Ut(){document.addEventListener("click",t=>{let e=t.target.closest("[data-action]");if(!e)return;let r=e.getAttribute("data-action");if(r==="copy-link")nt();else if(r==="toggle-clicks")H();else if(r==="download-json")at();else if(r==="confirm-delete"){let n=e.getAttribute("data-confirm")||"Are you sure?";confirm(n)||t.preventDefault()}})}function Ce(t,e,r){return!(e!=="all"&&t.dataset.activityKind!==e||r&&t.dataset.activityLevel!==r)}function Dt(){let t=document.getElementById("server-activity");if(!t)return;let e=Array.from(t.querySelectorAll("[data-activity-row]"));if(e.length===0)return;let r=t.querySelector("[data-activity-filter-type]"),n=t.querySelector("[data-activity-filter-level]");if(!r&&!n)return;let a="all",o="";function i(){e.forEach(s=>{let f=Ce(s,a,o);s.style.display=f?"":"none"})}r&&r.addEventListener("click",s=>{let f=s.target.closest("[data-filter-type]");f&&(a=f.dataset.filterType,Array.from(r.querySelectorAll("[data-filter-type]")).forEach(u=>{u.classList.toggle("btn-active",u===f),u.classList.toggle("btn-secondary",u!==f)}),i())}),n&&n.addEventListener("change",()=>{o=n.value,i()})}window.SentieroDashboard={initPlayer:rt,fetchAllEvents:F,copyLink:nt,downloadJSON:at,toggleClickOverlay:H,computeScrollDepth:Z,detectFrustrationEvents:G,analyzeFormInteractions:W,buildFormContext:D};function ve(){let t=document.getElementById("server-activity-markers");if(!t)return[];try{return mt(JSON.parse(t.textContent))}catch(e){return console.warn("Sentiero: failed to parse server-activity markers",e),[]}}document.addEventListener("DOMContentLoaded",()=>{let t=document.getElementById("sentiero-player-config");if(t)try{let e=JSON.parse(t.textContent),r=document.getElementById("replayer");if(e.eventsUrl&&r){let n=ve();rt(e.eventsUrl,r,n)}}catch(e){console.error("Sentiero: failed to parse player config",e)}Ut(),Dt(),window.addEventListener("pagehide",Q)});})();
|