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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ed5c945cbabf6b9ae1ab2b3e10474d06bc06bda78c1a4b381b69845791c18296
4
- data.tar.gz: abb85bfebbdb1e8c48cba2eb844b6446d5bd82f17e7727c3b00347b5256de38e
3
+ metadata.gz: e055280481854b52d5e5d4eba3a51a9f0e377d182b53acb2816aeeac51551ef8
4
+ data.tar.gz: 04dd008f190ecd97d78c75b6b99a23b32f1bac1a6f8b257edbba8ff89a4decda
5
5
  SHA512:
6
- metadata.gz: c5ef1855f03fc35d2b719ee316979de2ae3331b2400bfb534a0406048fe31cd9b292e252fdda5a59f54f73a066c33aed17aecaf88c2e697a82d0269ddd525f84
7
- data.tar.gz: c7fce88e343ca579a5add1656f6e12b997162a5c7dbe62630e21b00334e13e3bc88f51dcabc9878f8ad93712851b626f5475cd76bec870eb1f142a7089959021
6
+ metadata.gz: abc7b8ac2d64eb8e6df281f589c80b6eeaa4345cd3b47752e5c7a4d5d570ce7cfe482ab56d40424ab43d61cda956b3b02c49ebe9a329a364a1a533ad765a4228
7
+ data.tar.gz: 56a0d486e4189136a2546d4a72fc3d834fc0637c61fa3f4cf81bb9616a0acaf655555f23a9f9ef80e2ea92767b89986f3612f538609b022922bf9b56ca240cb1
@@ -16,21 +16,42 @@ module Sentiero
16
16
 
17
17
  MAX_FACET_VALUES = 50
18
18
 
19
- def grouped_errors(sort_by: "count", since: nil, until_time: nil)
19
+ STATUS_FILTERS = %w[open resolved ignored all].freeze
20
+
21
+ def grouped_errors(sort_by: "count", since: nil, until_time: nil, status: nil)
20
22
  groups = {}
21
23
 
22
24
  _scanned, hit_cap = scan_sessions(since: since, until_time: until_time, types: [META, CUSTOM]) do |summary, window_id, events|
23
25
  collect_window(groups, summary, window_id, events)
24
26
  end
25
27
 
28
+ groups.each_value { |group| group[:status] = effective_status(group) }
29
+
26
30
  {
27
- groups: sort_groups(groups.values, sort_by),
31
+ groups: sort_groups(filter_by_status(groups.values, status), sort_by),
28
32
  was_truncated: hit_cap
29
33
  }
30
34
  end
31
35
 
32
36
  private
33
37
 
38
+ def effective_status(group)
39
+ state = group[:state]
40
+ return "open" if state.nil?
41
+ return "ignored" if state[:status] == "ignored"
42
+ return "regressed" if group[:regressions].positive?
43
+
44
+ "resolved"
45
+ end
46
+
47
+ def filter_by_status(groups, status)
48
+ case status
49
+ when "all" then groups
50
+ when "resolved", "ignored" then groups.select { |g| g[:status] == status }
51
+ else groups.select { |g| %w[open regressed].include?(g[:status]) }
52
+ end
53
+ end
54
+
34
55
  def collect_window(groups, summary, window_id, events)
35
56
  anchor = window_anchor(summary, window_id, events)
36
57
 
@@ -51,6 +72,7 @@ module Sentiero
51
72
  group[:stack] ||= stack_of(payload)
52
73
  group[:count] += 1
53
74
  group[:last_seen_at] = [group[:last_seen_at], timestamp].compact.max
75
+ group[:regressions] += 1 if regression?(group[:state], timestamp)
54
76
  tally_facets(group, summary[:metadata] || {})
55
77
  return if group[:occurrences].size >= MAX_OCCURRENCES_PER_GROUP
56
78
 
@@ -67,15 +89,23 @@ module Sentiero
67
89
  payload.is_a?(Hash) ? payload : {}
68
90
  end
69
91
 
92
+ def regression?(state, timestamp)
93
+ state && state[:status] == "resolved" && timestamp && timestamp.to_f > state[:last_seen_at].to_f
94
+ end
95
+
70
96
  def new_group(key, message, payload)
97
+ id = Digest::SHA1.hexdigest(key)
71
98
  {
72
- id: Digest::SHA1.hexdigest(key),
99
+ id: id,
73
100
  message: message,
74
101
  source: source_of(payload),
75
102
  line: line_of(payload),
76
103
  stack: nil,
77
104
  count: 0,
78
105
  last_seen_at: nil,
106
+ state: @store.client_error_states([id])[id],
107
+ status: nil,
108
+ regressions: 0,
79
109
  browsers: Hash.new(0),
80
110
  devices: Hash.new(0),
81
111
  pages: Hash.new(0),
@@ -73,7 +73,7 @@ module Sentiero
73
73
  def build_errors
74
74
  headers = %w[message source line count last_seen_at session_id window_id offset_ms]
75
75
 
76
- rows = ErrorDiscovery.new(store).grouped_errors(since: since, until_time: until_time)[:groups].flat_map do |group|
76
+ rows = ErrorDiscovery.new(store).grouped_errors(since: since, until_time: until_time, status: "all")[:groups].flat_map do |group|
77
77
  group[:occurrences].map do |occ|
78
78
  [
79
79
  group[:message],
@@ -68,6 +68,19 @@ module Sentiero
68
68
  raise NoMethodError, "#{self.class}#session_ids_for_problem not implemented"
69
69
  end
70
70
 
71
+ def client_error_states(ids)
72
+ raise NoMethodError, "#{self.class}#client_error_states not implemented"
73
+ end
74
+
75
+ # changed_at is epoch seconds; last_seen_at is the group's epoch-ms watermark.
76
+ def set_client_error_state(id, status:, changed_at:, last_seen_at:)
77
+ raise NoMethodError, "#{self.class}#set_client_error_state not implemented"
78
+ end
79
+
80
+ def purge_client_error_states(cutoff)
81
+ raise NoMethodError, "#{self.class}#purge_client_error_states not implemented"
82
+ end
83
+
71
84
  private
72
85
 
73
86
  # Shared problem bookkeeping: the semantic source of truth for problem
@@ -123,6 +136,21 @@ module Sentiero
123
136
  )
124
137
  end
125
138
 
139
+ def client_error_state_attrs(status, changed_at, last_seen_at)
140
+ {status: status, changed_at: changed_at.to_f, last_seen_at: last_seen_at&.to_f}
141
+ end
142
+
143
+ def client_error_state_from_strings(h)
144
+ {status: h["status"], changed_at: h["changed_at"], last_seen_at: h["last_seen_at"]}
145
+ end
146
+
147
+ def select_client_error_states(states, ids)
148
+ ids.each_with_object({}) do |id, out|
149
+ state = states[id]
150
+ out[id] = state.dup if state
151
+ end
152
+ end
153
+
126
154
  def build_problem_title(occurrence)
127
155
  "#{occurrence["exception_class"]}: #{occurrence["message"]}"[0, PROBLEM_TITLE_MAX]
128
156
  end
@@ -291,6 +291,30 @@ module Sentiero
291
291
  latest_session_ids(occurrences[problem_id] || [], limit: limit)
292
292
  end
293
293
 
294
+ def client_error_states(ids)
295
+ select_client_error_states(read_client_error_states, ids)
296
+ end
297
+
298
+ def set_client_error_state(id, status:, changed_at:, last_seen_at:)
299
+ validate_id!(id)
300
+ validate_status!(status)
301
+ update_client_error_states do |states|
302
+ if status == "open"
303
+ states.delete(id)
304
+ else
305
+ states[id] = client_error_state_attrs(status, changed_at, last_seen_at)
306
+ end
307
+ end
308
+ nil
309
+ end
310
+
311
+ def purge_client_error_states(cutoff)
312
+ update_client_error_states do |states|
313
+ states.delete_if { |_id, state| state[:status] == "resolved" && state[:changed_at] < cutoff.to_f }
314
+ end
315
+ nil
316
+ end
317
+
294
318
  def clear!
295
319
  FileUtils.rm_rf(Dir.glob(::File.join(@root, "*")))
296
320
  nil
@@ -310,6 +334,7 @@ module Sentiero
310
334
  deleted = stale.size
311
335
 
312
336
  purge_error_data_older_than!(cutoff)
337
+ purge_client_error_states(cutoff)
313
338
 
314
339
  deleted
315
340
  end
@@ -552,6 +577,28 @@ module Sentiero
552
577
  }
553
578
  end
554
579
 
580
+ def read_client_error_states
581
+ raw = read_error_json("client_error_states.json") || {}
582
+ raw.transform_values { |s| client_error_state_from_strings(s) }
583
+ end
584
+
585
+ def update_client_error_states
586
+ ::File.open(error_lock_path, ::File::RDWR | ::File::CREAT, 0o600) do |lock|
587
+ lock.flock(::File::LOCK_EX)
588
+ states = read_client_error_states
589
+ yield states
590
+ raw = states.transform_values { |s| stringify_client_error_state(s) }
591
+ path = error_data_path("client_error_states.json")
592
+ tmp = "#{path}.tmp.#{Process.pid}"
593
+ ::File.write(tmp, JSON.generate(raw))
594
+ ::File.rename(tmp, path)
595
+ end
596
+ end
597
+
598
+ def stringify_client_error_state(s)
599
+ {"status" => s[:status], "changed_at" => s[:changed_at], "last_seen_at" => s[:last_seen_at]}
600
+ end
601
+
555
602
  def purge_error_data_older_than!(cutoff)
556
603
  update_error_data do |problems, occurrences, server_events|
557
604
  purge_error_collections!(problems, occurrences, server_events, cutoff)
@@ -15,6 +15,7 @@ module Sentiero
15
15
  @problems = Concurrent::Map.new # fingerprint => problem Hash (symbol-keyed)
16
16
  @occurrences = Concurrent::Map.new # fingerprint => Concurrent::Array of occurrence Hashes
17
17
  @server_events = Concurrent::Array.new
18
+ @client_error_states = Concurrent::Map.new
18
19
  end
19
20
 
20
21
  def save_events(ref, events)
@@ -260,9 +261,33 @@ module Sentiero
260
261
  latest_session_ids(list.to_a, limit: limit)
261
262
  end
262
263
 
264
+ def client_error_states(ids)
265
+ select_client_error_states(@client_error_states, ids)
266
+ end
267
+
268
+ def set_client_error_state(id, status:, changed_at:, last_seen_at:)
269
+ validate_id!(id)
270
+ validate_status!(status)
271
+ if status == "open"
272
+ @client_error_states.delete(id)
273
+ else
274
+ @client_error_states[id] = client_error_state_attrs(status, changed_at, last_seen_at)
275
+ end
276
+ nil
277
+ end
278
+
279
+ def purge_client_error_states(cutoff)
280
+ @client_error_states.each_pair do |id, state|
281
+ @client_error_states.delete(id) if state[:status] == "resolved" && state[:changed_at] < cutoff.to_f
282
+ end
283
+ nil
284
+ end
285
+
263
286
  def purge_older_than(seconds)
264
287
  deleted = super
265
- purge_error_collections!(@problems, @occurrences, @server_events, Time.now.to_f - seconds)
288
+ cutoff = Time.now.to_f - seconds
289
+ purge_error_collections!(@problems, @occurrences, @server_events, cutoff)
290
+ purge_client_error_states(cutoff)
266
291
  deleted
267
292
  end
268
293
 
@@ -271,6 +296,7 @@ module Sentiero
271
296
  @problems.clear
272
297
  @occurrences.clear
273
298
  @server_events.clear
299
+ @client_error_states.clear
274
300
  nil
275
301
  end
276
302
 
@@ -50,6 +50,10 @@ module Sentiero
50
50
  @server_events ||= "#{@prefix}server_events"
51
51
  end
52
52
 
53
+ def client_error_states
54
+ @client_error_states ||= "#{@prefix}client_error_states"
55
+ end
56
+
53
57
  def server_events_project(project)
54
58
  "#{@prefix}server_events:project:#{project}"
55
59
  end
@@ -406,6 +406,34 @@ module Sentiero
406
406
  latest_session_ids(raw.map { |json| JSON.parse(json) }, limit: limit)
407
407
  end
408
408
 
409
+ def client_error_states(ids)
410
+ return {} if ids.empty?
411
+
412
+ values = @redis.hmget(@keys.client_error_states, *ids)
413
+ ids.zip(values).each_with_object({}) do |(id, json), out|
414
+ out[id] = client_error_state_from_strings(JSON.parse(json)) if json
415
+ end
416
+ end
417
+
418
+ def set_client_error_state(id, status:, changed_at:, last_seen_at:)
419
+ validate_id!(id)
420
+ validate_status!(status)
421
+ if status == "open"
422
+ @redis.hdel(@keys.client_error_states, id)
423
+ else
424
+ @redis.hset(@keys.client_error_states, id, JSON.generate(client_error_state_attrs(status, changed_at, last_seen_at)))
425
+ end
426
+ nil
427
+ end
428
+
429
+ def purge_client_error_states(cutoff)
430
+ @redis.hscan_each(@keys.client_error_states) do |id, json|
431
+ state = JSON.parse(json)
432
+ @redis.hdel(@keys.client_error_states, id) if state["status"] == "resolved" && state["changed_at"].to_f < cutoff.to_f
433
+ end
434
+ nil
435
+ end
436
+
409
437
  def clear!
410
438
  # SCAN, not KEYS: KEYS is O(N) over the whole keyspace and blocks the server.
411
439
  @redis.scan_each(match: "#{@prefix}*") { |key| @redis.del(key) }
@@ -430,6 +458,7 @@ module Sentiero
430
458
  end
431
459
 
432
460
  purge_error_data_older_than!(cutoff)
461
+ purge_client_error_states(cutoff)
433
462
 
434
463
  deleted
435
464
  end
@@ -80,6 +80,13 @@ module Sentiero
80
80
  CREATE INDEX IF NOT EXISTS idx_server_events_event_id ON server_events(event_id);
81
81
  CREATE INDEX IF NOT EXISTS idx_server_events_project ON server_events(project);
82
82
  CREATE INDEX IF NOT EXISTS idx_server_events_session ON server_events(session_id);
83
+
84
+ CREATE TABLE IF NOT EXISTS client_error_states (
85
+ id TEXT PRIMARY KEY,
86
+ status TEXT NOT NULL,
87
+ changed_at REAL NOT NULL,
88
+ last_seen_at REAL
89
+ );
83
90
  SQL
84
91
 
85
92
  migrate_events_type!(db)
@@ -58,10 +58,25 @@ module Sentiero
58
58
  # a slow dashboard scan can't stall recorder ingest (and vice versa).
59
59
  @in_memory = @path == ":memory:" || @path.start_with?("file::memory:")
60
60
  @tls_key = :"sentiero_sqlite_#{object_id}"
61
+ # Every connection this store opened, across all threads, so #close can
62
+ # release them. Thread-locals alone outlive a discarded store on
63
+ # long-lived threads (puma workers, the test runner) and leak fds.
64
+ @connections = []
65
+ @connections_lock = Mutex.new
61
66
  @shared_db = create_connection if @in_memory
62
67
  Schema.create(db)
63
68
  end
64
69
 
70
+ # Closes every connection this store opened. A file-backed store reopens a
71
+ # connection on next use; an in-memory store's data is gone for good.
72
+ def close
73
+ @connections_lock.synchronize do
74
+ @connections.each { |conn| conn.close unless conn.closed? }
75
+ @connections.clear
76
+ end
77
+ nil
78
+ end
79
+
65
80
  # event_type_counts / event_counts_by_day group on indexed columns and
66
81
  # never read payloads (see Schema for the column-order guarantee).
67
82
  def supports_event_aggregates?
@@ -505,6 +520,36 @@ module Sentiero
505
520
  db.execute(sql, params).map { |row| row["session_id"] }
506
521
  end
507
522
 
523
+ def client_error_states(ids)
524
+ return {} if ids.empty?
525
+
526
+ placeholders = (["?"] * ids.size).join(", ")
527
+ rows = db.execute("SELECT id, status, changed_at, last_seen_at FROM client_error_states WHERE id IN (#{placeholders})", ids)
528
+ rows.each_with_object({}) do |row, out|
529
+ out[row["id"]] = {status: row["status"], changed_at: row["changed_at"], last_seen_at: row["last_seen_at"]}
530
+ end
531
+ end
532
+
533
+ def set_client_error_state(id, status:, changed_at:, last_seen_at:)
534
+ validate_id!(id)
535
+ validate_status!(status)
536
+ if status == "open"
537
+ db.execute("DELETE FROM client_error_states WHERE id = ?", [id])
538
+ else
539
+ db.execute(
540
+ "INSERT INTO client_error_states (id, status, changed_at, last_seen_at) VALUES (?, ?, ?, ?) " \
541
+ "ON CONFLICT(id) DO UPDATE SET status = excluded.status, changed_at = excluded.changed_at, last_seen_at = excluded.last_seen_at",
542
+ [id, status, changed_at.to_f, last_seen_at&.to_f]
543
+ )
544
+ end
545
+ nil
546
+ end
547
+
548
+ def purge_client_error_states(cutoff)
549
+ db.execute("DELETE FROM client_error_states WHERE status = 'resolved' AND changed_at < ?", [cutoff.to_f])
550
+ nil
551
+ end
552
+
508
553
  def clear!
509
554
  db.transaction(:immediate) do
510
555
  db.execute("DELETE FROM events")
@@ -512,6 +557,7 @@ module Sentiero
512
557
  db.execute("DELETE FROM problems")
513
558
  db.execute("DELETE FROM occurrences")
514
559
  db.execute("DELETE FROM server_events")
560
+ db.execute("DELETE FROM client_error_states")
515
561
  end
516
562
  nil
517
563
  end
@@ -535,6 +581,7 @@ module Sentiero
535
581
  [cutoff]
536
582
  )
537
583
  db.execute("DELETE FROM problems WHERE last_seen < ?", [cutoff])
584
+ db.execute("DELETE FROM client_error_states WHERE status = 'resolved' AND changed_at < ?", [cutoff])
538
585
  end
539
586
 
540
587
  session_count
@@ -609,7 +656,10 @@ module Sentiero
609
656
  def db
610
657
  return @shared_db if @in_memory
611
658
 
612
- Thread.current[@tls_key] ||= create_connection
659
+ conn = Thread.current[@tls_key]
660
+ return conn if conn && !conn.closed?
661
+
662
+ Thread.current[@tls_key] = create_connection
613
663
  end
614
664
 
615
665
  def create_connection
@@ -621,6 +671,7 @@ module Sentiero
621
671
  conn.busy_handler_timeout = 5_000
622
672
  conn.execute("PRAGMA journal_mode=WAL")
623
673
  conn.execute("PRAGMA foreign_keys=ON")
674
+ @connections_lock.synchronize { @connections << conn }
624
675
  conn
625
676
  end
626
677
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sentiero
4
- VERSION = "1.0.0.beta3"
4
+ VERSION = "1.0.0.beta4"
5
5
  end
@@ -0,0 +1,3 @@
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 ct(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 lt(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 Ft(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(!Ft(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 B(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 Yt(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 Vt(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 c=1;c<t.length;c++){let l=t[c].timestamp-t[c-1].timestamp;l>k&&(o.push({start:i,end:t[c-1].timestamp}),a.push({start:t[c-1].timestamp,end:t[c].timestamp,duration:l}),i=t[c].timestamp)}if(o.push({start:i,end:r}),a.length===0)return{map:c=>(c-e)/n,gaps:[]};let s=0;for(let c=0;c<o.length;c++)s+=o[c].end-o[c].start;let f=a.length*it;f>40&&(f=40);let u=f/a.length,m=100-f,d=[],p=0;for(let c=0;c<o.length;c++){let l=o[c].end-o[c].start,h=s>0?l/s*m:0;d.push({type:"active",start:o[c].start,end:o[c].end,posStart:p,posEnd:p+h}),p+=h,c<a.length&&(d.push({type:"gap",start:a[c].start,end:a[c].end,duration:a[c].duration,posStart:p,posEnd:p+u}),p+=u)}function g(c){for(let l=0;l<d.length;l++){let h=d[l];if(h.type==="active"&&c>=h.start&&c<=h.end){let v=h.end-h.start,Y=v>0?(c-h.start)/v:0;return(h.posStart+Y*(h.posEnd-h.posStart))/100}}for(let l=0;l<d.length;l++)if(d[l].type==="gap"&&c>=d[l].start&&c<=d[l].end)return d[l].posStart/100;return c<=e?0:1}let y=d.filter(c=>c.type==="gap");return{map:g,gaps:y}}function ut(t){if(!t||t.length===0)return[];let e=t[0].timestamp;j=Vt(t);let r=D(t),n=0,a=[];for(let o=0;o<t.length;o++){let i=Yt(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 d=C[m],p=document.createElement("span");p.className="marker-legend-item";let g=document.createElement("span");g.className="marker-legend-dot",g.style.backgroundColor=d.color,p.appendChild(g),p.appendChild(document.createTextNode(d.label)),n.appendChild(p)}),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 d=`${Math.max(1,Math.min(99,m.position))}%`;if(m.events.length===1){let p=m.events[0],g=C[p.category];if(!g)return;let y=document.createElement("div");y.className=`event-marker${p.category==="error"?" error-marker":""}`,y.style.left=d,y.style.backgroundColor=g.color,y.title=`${R(p)} at ${M(p.offset)}`,y.addEventListener("click",()=>P(p.offset)),a.appendChild(y)}else{let p=m.events.some(l=>l.category==="error"),g=document.createElement("div");g.className="marker-group",g.style.left=d;let y=document.createElement("div");y.className=`event-marker marker-group-dot${p?" error-marker":""}`,y.style.backgroundColor=p?"#ff0000":"#6c757d",y.title=`${m.events.length} events at ${M(m.events[0].offset)}`;let c=document.createElement("span");c.className="marker-group-count",c.textContent=m.events.length,g.appendChild(y),g.appendChild(c),g.addEventListener("click",l=>{l.stopPropagation(),J();let h=document.createElement("div");h.className="marker-dropdown",m.events.forEach(v=>{let Y=C[v.category]||{},N=document.createElement("div");N.className=`marker-dropdown-item${v.category==="error"?" error-entry":""}`;let V=document.createElement("span");V.className="activity-dot",V.style.backgroundColor=Y.color||"#6c757d";let ot=document.createElement("span");ot.textContent=`${M(v.offset)} ${R(v)}`,N.appendChild(V),N.appendChild(ot),N.addEventListener("click",Bt=>{Bt.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 d=document.createElement("div");d.className="marker-gap",d.style.left=`${m.posStart}%`,d.style.width=`${m.posEnd-m.posStart}%`,d.title=`Inactive: ${S(m.duration)}`,a.appendChild(d)}),ht||(ht=!0,document.addEventListener("click",J)),r.appendChild(a),r.style.display=""}var F=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,c=a.offset-y;if(c>k){let l=document.createElement("div");l.className="activity-gap",l.textContent=`${S(c)} inactive`,r.appendChild(l)}}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 d=document.createElement("span");d.className="activity-dot",d.style.backgroundColor=i.color;let p;a.isServerMarker&&a.href?(p=document.createElement("a"),p.href=a.href,p.className="activity-label activity-label-link",p.addEventListener("click",y=>y.stopPropagation())):(p=document.createElement("span"),p.className="activity-label"),p.textContent=R(a),f.appendChild(m),f.appendChild(d),f.appendChild(p),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(c=>{let l=document.createElement("div");if(l.className="activity-detail-line",c.indexOf(`
3
+ `)!==-1){let h=document.createElement("pre");h.className="activity-detail-pre",h.textContent=c,l.appendChild(h)}else l.textContent=c;y.appendChild(l)}),s.appendChild(y)}r.appendChild(s)}),qt()}}function Q(){F&&(clearInterval(F),F=null)}function qt(){Q(),F=setInterval(()=>{let t=A();if(t)try{let r=t.getReplayer().getCurrentTime();Jt(r),Et(r)}catch{}},250)}function Kt(t,e){if(!t)return;let r=e.getBoundingClientRect(),n=r.top-t.getBoundingClientRect().top-t.clientTop+t.scrollTop,a=n+r.height;n<t.scrollTop?t.scrollTo({top:n,behavior:"smooth"}):a>t.scrollTop+t.clientHeight&&t.scrollTo({top:a-t.clientHeight,behavior:"smooth"})}function Jt(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"),Kt(document.getElementById("activity-sidebar"),e[n])):a.classList.remove("active"))}}var Xt=["LCP","CLS","INP"];function zt(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 Qt(t,e){return t==="CLS"?e.toFixed(3):`${Math.round(e)} ms`}function Zt(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=zt(t);e.innerHTML="";let n=!1;for(let a of Xt){let o=r[a];if(!o)continue;n=!0;let i=document.createElement("span");i.className=`badge ${Zt(o.rating)} shrink-0`,i.textContent=`${a} ${Qt(a,o.value)}`,o.rating&&(i.title=o.rating),e.appendChild(i)}e.style.display=n?"inline-flex":"none"}function ee(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 re(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(ee(t));if(e<=0)return null;let r=re(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 ne(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 ae(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 oe(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 ie(){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 ce(){tt();let t=ie();if(!t)return;let e=U(),r=ae(e),n=ne(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}=oe(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?ce():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 se(t){return!t||!t.data?!1:t.type===4?!0:t.type===3?t.data.source===0||t.data.source===5:!1}function de(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 d=n;d<a;d++)m.push(e[d].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&&se(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=de(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 fe=1e4,ue=1e3;function me(t){return t.type===3&&t.data&&t.data.source>=1&&t.data.source<=5}function ye(t,e=fe){if(!Array.isArray(t)||t.length===0)return[];let r=t[0].timestamp,n=[0],a=!1;for(let i of t)me(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 he(t,e,r=ue){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=ye(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=he(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 ge(t){let e=document.getElementById("active-time");if(!e)return;let r=ct(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 Ee(t){let e=document.getElementById("viewport-info");if(!e)return;let r=lt(t);r&&(e.textContent=`Viewport: ${r}`,e.style.display="inline")}function xe(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 Ce(){$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}B(t,(n,a)=>{if(n){console.error("Sentiero: failed to load events",n),e.innerHTML="";let l=document.createElement("div");l.className="alert alert-danger",l.textContent=`Failed to load session events: ${n.message}`,e.appendChild(l);return}if(a.length===0){e.innerHTML='<p class="text-muted">No events recorded for this window.</p>';return}Pt=a,ge(a),Ee(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(l=>{l.position=i?i.map(l.timestamp):0});let u=Array.isArray(r)?r:[];u.forEach(l=>{l.position=i?i.map(s+l.offset):0});let m=o.concat(f).concat(u).sort((l,h)=>l.offset-h.offset);xt(m,a),Ct(m);let d=e.closest(".player-frame")||e.parentElement,p=d?d.clientWidth:1024,g=Math.min(2048,p),y=rrwebPlayer.default||rrwebPlayer;x=new y({target:e,props:{events:a,showController:!0,autoPlay:!1,skipInactive:!0,width:g}});let c=xe(a);if(c)try{let l=x.getReplayer();l&&setTimeout(()=>{l.play(c-a[0].timestamp),l.pause()},100)}catch(l){console.warn("Sentiero: could not seek to timestamp",l)}Lt(x,a),St(),Ce()})}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 ve(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=ve(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:B,copyLink:nt,downloadJSON:at,toggleClickOverlay:H,computeScrollDepth:Z,detectFrustrationEvents:G,analyzeFormInteractions:W,buildFormContext:D};function Te(){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=Te();rt(e.eventsUrl,r,n)}}catch(e){console.error("Sentiero: failed to parse player config",e)}Ut(),Dt(),window.addEventListener("pagehide",Q)});})();
@@ -1,6 +1,6 @@
1
1
  {
2
- "recorder": "recorder-W2RCU5VM.js",
3
- "dashboard": "dashboard-YINQE5ZH.js",
2
+ "recorder": "recorder-DCSAOXX4.js",
3
+ "dashboard": "dashboard-R7JYFY62.js",
4
4
  "sessions_index": "sessions_index-2RAGTEZM.js",
5
5
  "analytics": "analytics-RH24EOLD.js",
6
6
  "heatmap": "heatmap-EBKFWSKN.js",