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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e055280481854b52d5e5d4eba3a51a9f0e377d182b53acb2816aeeac51551ef8
|
|
4
|
+
data.tar.gz: 04dd008f190ecd97d78c75b6b99a23b32f1bac1a6f8b257edbba8ff89a4decda
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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:
|
|
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
|
data/lib/sentiero/stores/file.rb
CHANGED
|
@@ -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
|
-
|
|
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]
|
|
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
|
|
data/lib/sentiero/version.rb
CHANGED
|
@@ -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-
|
|
3
|
-
"dashboard": "dashboard-
|
|
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",
|