catpm 0.4.0 → 0.6.0
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/README.md +1 -1
- data/app/controllers/catpm/endpoints_controller.rb +56 -1
- data/app/controllers/catpm/events_controller.rb +75 -9
- data/app/controllers/catpm/samples_controller.rb +11 -0
- data/app/controllers/catpm/status_controller.rb +13 -2
- data/app/models/catpm/endpoint_pref.rb +14 -0
- data/app/models/catpm/event_pref.rb +14 -0
- data/app/views/catpm/endpoints/ignored.html.erb +57 -0
- data/app/views/catpm/endpoints/show.html.erb +7 -3
- data/app/views/catpm/events/ignored.html.erb +52 -0
- data/app/views/catpm/events/index.html.erb +15 -1
- data/app/views/catpm/events/show.html.erb +13 -4
- data/app/views/catpm/samples/show.html.erb +12 -7
- data/app/views/catpm/shared/_segments_waterfall.html.erb +5 -0
- data/app/views/catpm/status/index.html.erb +18 -2
- data/app/views/layouts/catpm/application.html.erb +130 -0
- data/config/routes.rb +8 -2
- data/db/migrate/20250601000001_create_catpm_tables.rb +22 -0
- data/lib/catpm/adapter/base.rb +8 -4
- data/lib/catpm/call_tracer.rb +85 -0
- data/lib/catpm/collector.rb +76 -19
- data/lib/catpm/configuration.rb +22 -1
- data/lib/catpm/fingerprint.rb +2 -2
- data/lib/catpm/flusher.rb +45 -32
- data/lib/catpm/middleware.rb +7 -0
- data/lib/catpm/request_segments.rb +2 -2
- data/lib/catpm/segment_subscribers.rb +4 -2
- data/lib/catpm/stack_sampler.rb +5 -3
- data/lib/catpm/trace.rb +9 -1
- data/lib/catpm/version.rb +1 -1
- data/lib/catpm.rb +1 -0
- data/lib/generators/catpm/templates/initializer.rb.tt +69 -57
- data/lib/tasks/catpm_tasks.rake +28 -0
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 72b1f8ad18b14d62b8ace369a8d064396ff4fc2988f1dc4eb911c71278c05766
|
|
4
|
+
data.tar.gz: f6153a851adc6ff5dd810cf648b069b512117bd25ae87f6e7715382ea6c43960
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 485696711db8a1ed56901065ee90b151292a466c4a21bf6585e63e36a0ee6f873a14584b229f1963bd9fa14ac5143d4fbc3641763177dec0ec2fc5c5421ba11d
|
|
7
|
+
data.tar.gz: c6e0dd6391af5921e4fa79ad77a69fe7d637adf77f714cf19eb66182dbac128c7c45d75ee1ac7365620824afc34d6672d788169c230c72d8ac0af27460167eff
|
data/README.md
CHANGED
|
@@ -85,6 +85,7 @@ module Catpm
|
|
|
85
85
|
@samples = endpoint_samples.where(sample_type: 'random').order(recorded_at: :desc).limit(10)
|
|
86
86
|
@error_samples = endpoint_samples.where(sample_type: 'error').order(recorded_at: :desc).limit(10)
|
|
87
87
|
|
|
88
|
+
@pref = Catpm::EndpointPref.find_by(kind: @kind, target: @target, operation: @operation)
|
|
88
89
|
@active_error_count = Catpm::ErrorRecord.unresolved.count
|
|
89
90
|
end
|
|
90
91
|
|
|
@@ -94,7 +95,61 @@ module Catpm
|
|
|
94
95
|
operation = params[:operation].presence || ''
|
|
95
96
|
|
|
96
97
|
Catpm::Bucket.where(kind: kind, target: target, operation: operation).destroy_all
|
|
97
|
-
|
|
98
|
+
Catpm::EndpointPref.find_by(kind: kind, target: target, operation: operation)&.destroy
|
|
99
|
+
if request.xhr?
|
|
100
|
+
render json: { deleted: true }
|
|
101
|
+
else
|
|
102
|
+
redirect_to catpm.status_index_path, notice: 'Endpoint deleted'
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def toggle_pin
|
|
107
|
+
pref = Catpm::EndpointPref.lookup(params[:kind], params[:target], params[:operation])
|
|
108
|
+
pref.pinned = !pref.pinned
|
|
109
|
+
pref.save!
|
|
110
|
+
if request.xhr?
|
|
111
|
+
render json: { pinned: pref.pinned }
|
|
112
|
+
else
|
|
113
|
+
redirect_back fallback_location: catpm.endpoint_path(kind: params[:kind], target: params[:target], operation: params[:operation])
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def toggle_ignore
|
|
118
|
+
pref = Catpm::EndpointPref.lookup(params[:kind], params[:target], params[:operation])
|
|
119
|
+
pref.ignored = !pref.ignored
|
|
120
|
+
pref.save!
|
|
121
|
+
if request.xhr?
|
|
122
|
+
render json: { ignored: pref.ignored }
|
|
123
|
+
else
|
|
124
|
+
redirect_back fallback_location: catpm.status_index_path
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def ignored
|
|
129
|
+
@range, period, _bucket_seconds = helpers.parse_range(remembered_range)
|
|
130
|
+
ignored_prefs = Catpm::EndpointPref.ignored
|
|
131
|
+
|
|
132
|
+
scope = @range == 'all' ? Catpm::Bucket.all : Catpm::Bucket.recent(period)
|
|
133
|
+
grouped = scope.group_by { |b| [b.kind, b.target, b.operation] }
|
|
134
|
+
|
|
135
|
+
ignored_keys = ignored_prefs.map { |p| [p.kind, p.target, p.operation] }.to_set
|
|
136
|
+
|
|
137
|
+
@ignored_endpoints = ignored_prefs.map do |pref|
|
|
138
|
+
key = [pref.kind, pref.target, pref.operation]
|
|
139
|
+
bs = grouped[key]
|
|
140
|
+
total_count = bs ? bs.sum(&:count) : 0
|
|
141
|
+
{
|
|
142
|
+
kind: pref.kind,
|
|
143
|
+
target: pref.target,
|
|
144
|
+
operation: pref.operation,
|
|
145
|
+
total_count: total_count,
|
|
146
|
+
avg_duration: total_count > 0 ? bs.sum(&:duration_sum) / total_count : 0.0,
|
|
147
|
+
last_seen: bs&.map(&:bucket_start)&.max
|
|
148
|
+
}
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
@active_endpoint_count = grouped.keys.count { |k| !ignored_keys.include?(k) }
|
|
152
|
+
@active_error_count = Catpm::ErrorRecord.unresolved.count
|
|
98
153
|
end
|
|
99
154
|
end
|
|
100
155
|
end
|
|
@@ -30,40 +30,48 @@ module Catpm
|
|
|
30
30
|
grouped = recent_buckets.group_by(&:name)
|
|
31
31
|
@unique_names = grouped.keys.size
|
|
32
32
|
|
|
33
|
+
# Load event preferences
|
|
34
|
+
prefs = Catpm::EventPref.where('pinned = ? OR ignored = ?', true, true).index_by(&:name)
|
|
35
|
+
|
|
36
|
+
now_slot = (Time.current.to_i / bucket_seconds) * bucket_seconds
|
|
37
|
+
@sparkline_times = 60.times.map { |i| Time.at(now_slot - (59 - i) * bucket_seconds).strftime('%H:%M') }
|
|
38
|
+
|
|
33
39
|
events_list = grouped.map do |name, bs|
|
|
34
40
|
total_count = bs.sum(&:count)
|
|
35
41
|
|
|
36
|
-
# Sparkline data for this name
|
|
37
42
|
slots = {}
|
|
38
43
|
bs.each do |b|
|
|
39
44
|
slot_key = (b.bucket_start.to_i / bucket_seconds) * bucket_seconds
|
|
40
45
|
slots[slot_key] = (slots[slot_key] || 0) + b.count
|
|
41
46
|
end
|
|
42
|
-
now_slot = (Time.current.to_i / bucket_seconds) * bucket_seconds
|
|
43
47
|
sparkline = 60.times.map { |i| slots[now_slot - (59 - i) * bucket_seconds] || 0 }
|
|
44
48
|
|
|
49
|
+
pref = prefs[name]
|
|
45
50
|
{
|
|
46
51
|
name: name,
|
|
47
52
|
total_count: total_count,
|
|
48
53
|
sparkline: sparkline,
|
|
49
|
-
last_seen: bs.map(&:bucket_start).max
|
|
54
|
+
last_seen: bs.map(&:bucket_start).max,
|
|
55
|
+
pinned: pref&.pinned || false,
|
|
56
|
+
ignored: pref&.ignored || false
|
|
50
57
|
}
|
|
51
58
|
end
|
|
52
59
|
|
|
53
|
-
#
|
|
60
|
+
# Separate ignored events
|
|
61
|
+
@ignored_events = events_list.select { |e| e[:ignored] }
|
|
62
|
+
events_list = events_list.reject { |e| e[:ignored] }
|
|
63
|
+
|
|
64
|
+
# Sort (pinned always on top)
|
|
54
65
|
@sort = %w[name total_count last_seen].include?(params[:sort]) ? params[:sort] : 'total_count'
|
|
55
66
|
@dir = params[:dir] == 'asc' ? 'asc' : 'desc'
|
|
56
67
|
events_list = events_list.sort_by { |e| e[@sort.to_sym] || '' }
|
|
57
68
|
events_list = events_list.reverse if @dir == 'desc'
|
|
69
|
+
events_list = events_list.sort_by { |e| e[:pinned] ? 0 : 1 }
|
|
58
70
|
|
|
59
71
|
@total_event_names = events_list.size
|
|
60
72
|
|
|
61
|
-
# Sparkline times for tooltips
|
|
62
|
-
now_slot = (Time.current.to_i / bucket_seconds) * bucket_seconds
|
|
63
|
-
@sparkline_times = 60.times.map { |i| Time.at(now_slot - (59 - i) * bucket_seconds).strftime('%H:%M') }
|
|
64
|
-
|
|
65
73
|
# Pagination
|
|
66
|
-
@page = [
|
|
74
|
+
@page = [params[:page].to_i, 1].max
|
|
67
75
|
@events = events_list.drop((@page - 1) * PER_PAGE).first(PER_PAGE)
|
|
68
76
|
|
|
69
77
|
@active_error_count = Catpm::ErrorRecord.unresolved.count
|
|
@@ -107,6 +115,64 @@ module Catpm
|
|
|
107
115
|
# Recent samples
|
|
108
116
|
@samples = Catpm::EventSample.by_name(@name).order(recorded_at: :desc).limit(Catpm.config.events_max_samples_per_name)
|
|
109
117
|
|
|
118
|
+
@pref = Catpm::EventPref.find_by(name: @name)
|
|
119
|
+
@active_error_count = Catpm::ErrorRecord.unresolved.count
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def destroy
|
|
123
|
+
name = params[:name]
|
|
124
|
+
Catpm::EventBucket.where(name: name).destroy_all
|
|
125
|
+
Catpm::EventSample.where(name: name).destroy_all
|
|
126
|
+
Catpm::EventPref.find_by(name: name)&.destroy
|
|
127
|
+
if request.xhr?
|
|
128
|
+
render json: { deleted: true }
|
|
129
|
+
else
|
|
130
|
+
redirect_to catpm.events_path, notice: 'Event deleted'
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def toggle_pin
|
|
135
|
+
pref = Catpm::EventPref.lookup(params[:name])
|
|
136
|
+
pref.pinned = !pref.pinned
|
|
137
|
+
pref.save!
|
|
138
|
+
if request.xhr?
|
|
139
|
+
render json: { pinned: pref.pinned }
|
|
140
|
+
else
|
|
141
|
+
redirect_back fallback_location: catpm.event_path(name: params[:name])
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def toggle_ignore
|
|
146
|
+
pref = Catpm::EventPref.lookup(params[:name])
|
|
147
|
+
pref.ignored = !pref.ignored
|
|
148
|
+
pref.save!
|
|
149
|
+
if request.xhr?
|
|
150
|
+
render json: { ignored: pref.ignored }
|
|
151
|
+
else
|
|
152
|
+
redirect_back fallback_location: catpm.events_path
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def ignored
|
|
157
|
+
@range, period, _bucket_seconds = helpers.parse_range(remembered_range)
|
|
158
|
+
ignored_prefs = Catpm::EventPref.ignored
|
|
159
|
+
|
|
160
|
+
scope = @range == 'all' ? Catpm::EventBucket.all : Catpm::EventBucket.recent(period)
|
|
161
|
+
grouped = scope.group_by(&:name)
|
|
162
|
+
|
|
163
|
+
ignored_keys = ignored_prefs.map(&:name).to_set
|
|
164
|
+
|
|
165
|
+
@ignored_events = ignored_prefs.map do |pref|
|
|
166
|
+
bs = grouped[pref.name]
|
|
167
|
+
total_count = bs ? bs.sum(&:count) : 0
|
|
168
|
+
{
|
|
169
|
+
name: pref.name,
|
|
170
|
+
total_count: total_count,
|
|
171
|
+
last_seen: bs&.map(&:bucket_start)&.max
|
|
172
|
+
}
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
@active_event_count = grouped.keys.count { |k| !ignored_keys.include?(k) }
|
|
110
176
|
@active_error_count = Catpm::ErrorRecord.unresolved.count
|
|
111
177
|
end
|
|
112
178
|
end
|
|
@@ -12,5 +12,16 @@ module Catpm
|
|
|
12
12
|
Catpm::ErrorRecord.find_by(fingerprint: @sample.error_fingerprint)
|
|
13
13
|
end
|
|
14
14
|
end
|
|
15
|
+
|
|
16
|
+
def destroy
|
|
17
|
+
sample = Catpm::Sample.find(params[:id])
|
|
18
|
+
bucket = sample.bucket
|
|
19
|
+
sample.destroy
|
|
20
|
+
if bucket
|
|
21
|
+
redirect_to catpm.endpoint_path(kind: bucket.kind, target: bucket.target, operation: bucket.operation), notice: 'Sample deleted'
|
|
22
|
+
else
|
|
23
|
+
redirect_to catpm.status_index_path, notice: 'Sample deleted'
|
|
24
|
+
end
|
|
25
|
+
end
|
|
15
26
|
end
|
|
16
27
|
end
|
|
@@ -52,9 +52,13 @@ module Catpm
|
|
|
52
52
|
# Endpoints — aggregated from the SAME time range as hero metrics
|
|
53
53
|
grouped = recent_buckets.group_by { |b| [b.kind, b.target, b.operation] }
|
|
54
54
|
|
|
55
|
+
# Load endpoint preferences (pinned/ignored)
|
|
56
|
+
prefs = Catpm::EndpointPref.where('pinned = ? OR ignored = ?', true, true).index_by { |p| [p.kind, p.target, p.operation] }
|
|
57
|
+
|
|
55
58
|
endpoints = grouped.map do |key, bs|
|
|
56
59
|
kind, target, operation = key
|
|
57
60
|
total_count = bs.sum(&:count)
|
|
61
|
+
pref = prefs[key]
|
|
58
62
|
{
|
|
59
63
|
kind: kind,
|
|
60
64
|
target: target,
|
|
@@ -63,20 +67,27 @@ module Catpm
|
|
|
63
67
|
avg_duration: total_count > 0 ? bs.sum(&:duration_sum) / total_count : 0.0,
|
|
64
68
|
max_duration: bs.map(&:duration_max).max,
|
|
65
69
|
total_failures: bs.sum(&:failure_count),
|
|
66
|
-
last_seen: bs.map(&:bucket_start).max
|
|
70
|
+
last_seen: bs.map(&:bucket_start).max,
|
|
71
|
+
pinned: pref&.pinned || false,
|
|
72
|
+
ignored: pref&.ignored || false
|
|
67
73
|
}
|
|
68
74
|
end
|
|
69
75
|
|
|
76
|
+
# Separate ignored endpoints
|
|
77
|
+
@ignored_endpoints = endpoints.select { |e| e[:ignored] }
|
|
78
|
+
endpoints = endpoints.reject { |e| e[:ignored] }
|
|
79
|
+
|
|
70
80
|
# Kind filter (URL-based)
|
|
71
81
|
@available_kinds = endpoints.map { |e| e[:kind] }.uniq.sort
|
|
72
82
|
@kind_filter = params[:kind] if params[:kind].present? && @available_kinds.include?(params[:kind])
|
|
73
83
|
endpoints = endpoints.select { |e| e[:kind] == @kind_filter } if @kind_filter
|
|
74
84
|
|
|
75
|
-
# Server-side sort
|
|
85
|
+
# Server-side sort (pinned always on top)
|
|
76
86
|
@sort = %w[target total_count avg_duration max_duration total_failures last_seen].include?(params[:sort]) ? params[:sort] : 'last_seen'
|
|
77
87
|
@dir = params[:dir] == 'asc' ? 'asc' : 'desc'
|
|
78
88
|
endpoints = endpoints.sort_by { |e| e[@sort.to_sym] || '' }
|
|
79
89
|
endpoints = endpoints.reverse if @dir == 'desc'
|
|
90
|
+
endpoints = endpoints.sort_by { |e| e[:pinned] ? 0 : 1 }
|
|
80
91
|
|
|
81
92
|
@total_endpoint_count = endpoints.size
|
|
82
93
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Catpm
|
|
4
|
+
class EndpointPref < ActiveRecord::Base
|
|
5
|
+
self.table_name = 'catpm_endpoint_prefs'
|
|
6
|
+
|
|
7
|
+
scope :pinned, -> { where(pinned: true) }
|
|
8
|
+
scope :ignored, -> { where(ignored: true) }
|
|
9
|
+
|
|
10
|
+
def self.lookup(kind, target, operation)
|
|
11
|
+
find_or_initialize_by(kind: kind, target: target, operation: operation.presence || '')
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Catpm
|
|
4
|
+
class EventPref < ActiveRecord::Base
|
|
5
|
+
self.table_name = 'catpm_event_prefs'
|
|
6
|
+
|
|
7
|
+
scope :pinned, -> { where(pinned: true) }
|
|
8
|
+
scope :ignored, -> { where(ignored: true) }
|
|
9
|
+
|
|
10
|
+
def self.lookup(name)
|
|
11
|
+
find_or_initialize_by(name: name)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<% content_for :title, "Ignored Endpoints" %>
|
|
2
|
+
<% content_for :subtitle, "Endpoints hidden from the main dashboard" %>
|
|
3
|
+
|
|
4
|
+
<%= render "catpm/shared/page_nav", active: "performance" %>
|
|
5
|
+
|
|
6
|
+
<div class="tabs">
|
|
7
|
+
<a href="<%= catpm.status_index_path %>" class="tab">Active (<%= @active_endpoint_count %>)</a>
|
|
8
|
+
<a href="<%= catpm.ignored_endpoints_path %>" class="tab active">Ignored (<%= @ignored_endpoints.size %>)</a>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<%# ─── Time Range ─── %>
|
|
12
|
+
<div class="time-range">
|
|
13
|
+
<% (["all"] + Catpm::ApplicationHelper::RANGE_KEYS).each do |r| %>
|
|
14
|
+
<a href="<%= catpm.ignored_endpoints_path(range: r) %>" class="<%= 'active' if @range == r %>"><%= r == "all" ? "All" : r %></a>
|
|
15
|
+
<% end %>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<% if @ignored_endpoints.any? %>
|
|
19
|
+
<div class="table-scroll">
|
|
20
|
+
<table>
|
|
21
|
+
<thead>
|
|
22
|
+
<tr>
|
|
23
|
+
<th>Kind</th>
|
|
24
|
+
<th>Target</th>
|
|
25
|
+
<th>Count</th>
|
|
26
|
+
<th>Avg</th>
|
|
27
|
+
<th>Last Seen</th>
|
|
28
|
+
<th></th>
|
|
29
|
+
</tr>
|
|
30
|
+
</thead>
|
|
31
|
+
<tbody>
|
|
32
|
+
<% @ignored_endpoints.each do |ep| %>
|
|
33
|
+
<% ep_p = { kind: ep[:kind], target: ep[:target], operation: ep[:operation] } %>
|
|
34
|
+
<tr class="linked">
|
|
35
|
+
<td><a href="<%= catpm.endpoint_path(ep_p) %>" class="row-link"><%= type_badge(ep[:kind]) %></a></td>
|
|
36
|
+
<td class="mono"><%= ep[:target] %><%= " #{ep[:operation]}" if ep[:operation].present? %></td>
|
|
37
|
+
<td><%= ep[:total_count] %></td>
|
|
38
|
+
<td class="mono"><%= format_duration(ep[:avg_duration]) %></td>
|
|
39
|
+
<td><%= ep[:last_seen] ? time_with_tooltip(ep[:last_seen]) : "—" %></td>
|
|
40
|
+
<td style="position:relative; z-index:2; width:28px; padding:4px 2px; text-align:center">
|
|
41
|
+
<button class="action-menu-btn">⋮</button>
|
|
42
|
+
<div class="action-menu">
|
|
43
|
+
<button data-action="unignore" data-url="<%= catpm.endpoint_ignore_path(ep_p) %>">Unignore</button>
|
|
44
|
+
<button class="menu-danger" data-action="delete" data-url="<%= catpm.endpoint_path(ep_p) %>">Delete</button>
|
|
45
|
+
</div>
|
|
46
|
+
</td>
|
|
47
|
+
</tr>
|
|
48
|
+
<% end %>
|
|
49
|
+
</tbody>
|
|
50
|
+
</table>
|
|
51
|
+
</div>
|
|
52
|
+
<% else %>
|
|
53
|
+
<div class="empty-state">
|
|
54
|
+
<div class="empty-title">No ignored endpoints</div>
|
|
55
|
+
<div class="empty-hint">Endpoints you ignore will appear here.</div>
|
|
56
|
+
</div>
|
|
57
|
+
<% end %>
|
|
@@ -14,9 +14,13 @@
|
|
|
14
14
|
<span class="sep">/</span>
|
|
15
15
|
<span><%= @target %></span>
|
|
16
16
|
</div>
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
<div style="display:flex; gap:6px; align-items:center">
|
|
18
|
+
<%= button_to @pref&.ignored ? "Unignore" : "Ignore", catpm.endpoint_ignore_path(kind: @kind, target: @target, operation: @operation),
|
|
19
|
+
method: :patch, class: "btn" %>
|
|
20
|
+
<%= button_to "Delete Endpoint", catpm.endpoint_path(kind: @kind, target: @target, operation: @operation),
|
|
21
|
+
method: :delete, class: "btn btn-danger",
|
|
22
|
+
data: { confirm: "Delete this endpoint and all its data? This cannot be undone." } %>
|
|
23
|
+
</div>
|
|
20
24
|
</div>
|
|
21
25
|
|
|
22
26
|
<% ep_params = { kind: @kind, target: @target, operation: @operation } %>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<% content_for :title, "Ignored Events" %>
|
|
2
|
+
<% content_for :subtitle, "Events hidden from the main dashboard" %>
|
|
3
|
+
|
|
4
|
+
<%= render "catpm/shared/page_nav", active: "events" %>
|
|
5
|
+
|
|
6
|
+
<div class="tabs">
|
|
7
|
+
<a href="<%= catpm.events_path %>" class="tab">Active (<%= @active_event_count %>)</a>
|
|
8
|
+
<a href="<%= catpm.ignored_events_path %>" class="tab active">Ignored (<%= @ignored_events.size %>)</a>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<%# ─── Time Range ─── %>
|
|
12
|
+
<div class="time-range">
|
|
13
|
+
<% (["all"] + Catpm::ApplicationHelper::RANGE_KEYS).each do |r| %>
|
|
14
|
+
<a href="<%= catpm.ignored_events_path(range: r) %>" class="<%= 'active' if @range == r %>"><%= r == "all" ? "All" : r %></a>
|
|
15
|
+
<% end %>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<% if @ignored_events.any? %>
|
|
19
|
+
<div class="table-scroll">
|
|
20
|
+
<table>
|
|
21
|
+
<thead>
|
|
22
|
+
<tr>
|
|
23
|
+
<th>Name</th>
|
|
24
|
+
<th>Count</th>
|
|
25
|
+
<th>Last Seen</th>
|
|
26
|
+
<th></th>
|
|
27
|
+
</tr>
|
|
28
|
+
</thead>
|
|
29
|
+
<tbody>
|
|
30
|
+
<% @ignored_events.each do |ev| %>
|
|
31
|
+
<tr class="linked">
|
|
32
|
+
<td><a href="<%= catpm.event_path(name: ev[:name]) %>" class="row-link"><span class="badge badge-event"><%= ev[:name] %></span></a></td>
|
|
33
|
+
<td><%= ev[:total_count] %></td>
|
|
34
|
+
<td><%= ev[:last_seen] ? time_with_tooltip(ev[:last_seen]) : "—" %></td>
|
|
35
|
+
<td style="position:relative; z-index:2; width:28px; padding:4px 2px; text-align:center">
|
|
36
|
+
<button class="action-menu-btn">⋮</button>
|
|
37
|
+
<div class="action-menu">
|
|
38
|
+
<button data-action="unignore" data-url="<%= catpm.event_ignore_path(name: ev[:name]) %>">Unignore</button>
|
|
39
|
+
<button class="menu-danger" data-action="delete" data-url="<%= catpm.event_path(name: ev[:name]) %>">Delete</button>
|
|
40
|
+
</div>
|
|
41
|
+
</td>
|
|
42
|
+
</tr>
|
|
43
|
+
<% end %>
|
|
44
|
+
</tbody>
|
|
45
|
+
</table>
|
|
46
|
+
</div>
|
|
47
|
+
<% else %>
|
|
48
|
+
<div class="empty-state">
|
|
49
|
+
<div class="empty-title">No ignored events</div>
|
|
50
|
+
<div class="empty-hint">Events you ignore will appear here.</div>
|
|
51
|
+
</div>
|
|
52
|
+
<% end %>
|
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
|
|
4
4
|
<%= render "catpm/shared/page_nav", active: "events" %>
|
|
5
5
|
|
|
6
|
+
<div class="tabs">
|
|
7
|
+
<a href="<%= catpm.events_path(range: @range) %>" class="tab active">Active (<%= @total_event_names %>)</a>
|
|
8
|
+
<a href="<%= catpm.ignored_events_path %>" class="tab">Ignored (<%= @ignored_events.size %>)</a>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
6
11
|
<%# ─── Time Range ─── %>
|
|
7
12
|
<div class="time-range">
|
|
8
13
|
<% (["all"] + Catpm::ApplicationHelper::RANGE_KEYS).each do |r| %>
|
|
@@ -34,22 +39,24 @@
|
|
|
34
39
|
|
|
35
40
|
<%# ─── Events Table ─── %>
|
|
36
41
|
<h2>Events <% if @total_event_names > @events.size %><span class="text-muted" style="font-weight:400; font-size:13px">(showing <%= @events.size %> of <%= @total_event_names %>)</span><% end %></h2>
|
|
37
|
-
<%= section_description("Events tracked in the selected time range.") %>
|
|
38
42
|
|
|
39
43
|
<% if @events.any? %>
|
|
40
44
|
<div class="table-scroll">
|
|
41
45
|
<table>
|
|
42
46
|
<thead>
|
|
43
47
|
<tr>
|
|
48
|
+
<th></th>
|
|
44
49
|
<th><%= sort_header("Name", "name", @sort, @dir, extra_params: { range: @range }) %></th>
|
|
45
50
|
<th><%= sort_header("Count", "total_count", @sort, @dir, extra_params: { range: @range }) %></th>
|
|
46
51
|
<th>Trend</th>
|
|
47
52
|
<th><%= sort_header("Last Seen", "last_seen", @sort, @dir, extra_params: { range: @range }) %></th>
|
|
53
|
+
<th></th>
|
|
48
54
|
</tr>
|
|
49
55
|
</thead>
|
|
50
56
|
<tbody>
|
|
51
57
|
<% @events.each do |ev| %>
|
|
52
58
|
<tr class="linked">
|
|
59
|
+
<td style="position:relative; z-index:1; width:28px; padding:4px 2px; text-align:center"><button class="star-btn<%= ' pinned' if ev[:pinned] %>" data-pin-url="<%= catpm.event_pin_path(name: ev[:name]) %>"><%= ev[:pinned] ? "★".html_safe : "☆".html_safe %></button></td>
|
|
53
60
|
<td>
|
|
54
61
|
<a href="<%= catpm.event_path(name: ev[:name], range: @range) %>" class="row-link">
|
|
55
62
|
<span class="badge badge-event"><%= ev[:name] %></span>
|
|
@@ -58,6 +65,13 @@
|
|
|
58
65
|
<td><%= ev[:total_count] %></td>
|
|
59
66
|
<td><%= sparkline_svg(ev[:sparkline], width: 120, height: 32, color: "var(--accent)", time_labels: @sparkline_times) %></td>
|
|
60
67
|
<td><%= time_with_tooltip(ev[:last_seen]) %></td>
|
|
68
|
+
<td style="position:relative; z-index:2; width:28px; padding:4px 2px; text-align:center">
|
|
69
|
+
<button class="action-menu-btn">⋮</button>
|
|
70
|
+
<div class="action-menu">
|
|
71
|
+
<button data-action="ignore" data-url="<%= catpm.event_ignore_path(name: ev[:name]) %>">Ignore</button>
|
|
72
|
+
<button class="menu-danger" data-action="delete" data-url="<%= catpm.event_path(name: ev[:name]) %>">Delete</button>
|
|
73
|
+
</div>
|
|
74
|
+
</td>
|
|
61
75
|
</tr>
|
|
62
76
|
<% end %>
|
|
63
77
|
</tbody>
|
|
@@ -3,10 +3,19 @@
|
|
|
3
3
|
|
|
4
4
|
<%= render "catpm/shared/page_nav", active: "events" %>
|
|
5
5
|
|
|
6
|
-
<div class="breadcrumbs">
|
|
7
|
-
<
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
<div class="breadcrumbs" style="display:flex; align-items:center; justify-content:space-between">
|
|
7
|
+
<div>
|
|
8
|
+
<a href="<%= catpm.events_path(range: @range) %>">Events</a>
|
|
9
|
+
<span class="sep">/</span>
|
|
10
|
+
<span class="badge badge-event"><%= @name %></span>
|
|
11
|
+
</div>
|
|
12
|
+
<div style="display:flex; gap:6px; align-items:center">
|
|
13
|
+
<%= button_to @pref&.ignored ? "Unignore" : "Ignore", catpm.event_ignore_path(name: @name),
|
|
14
|
+
method: :patch, class: "btn" %>
|
|
15
|
+
<%= button_to "Delete Event", catpm.event_path(name: @name),
|
|
16
|
+
method: :delete, class: "btn btn-danger",
|
|
17
|
+
data: { confirm: "Delete this event and all its data? This cannot be undone." } %>
|
|
18
|
+
</div>
|
|
10
19
|
</div>
|
|
11
20
|
|
|
12
21
|
<%# ─── Time Range ─── %>
|
|
@@ -6,14 +6,19 @@
|
|
|
6
6
|
· <%= format_duration(@sample.duration) %>
|
|
7
7
|
<% end %>
|
|
8
8
|
|
|
9
|
-
<div class="breadcrumbs">
|
|
10
|
-
<
|
|
11
|
-
|
|
9
|
+
<div class="breadcrumbs" style="display:flex; align-items:center; justify-content:space-between">
|
|
10
|
+
<div>
|
|
11
|
+
<a href="<%= catpm.status_index_path %>">Overview</a>
|
|
12
|
+
<% if @bucket %>
|
|
13
|
+
<span class="sep">/</span>
|
|
14
|
+
<a href="<%= catpm.endpoint_path(kind: @bucket.kind, target: @bucket.target, operation: @bucket.operation) %>"><%= @bucket.target %></a>
|
|
15
|
+
<% end %>
|
|
12
16
|
<span class="sep">/</span>
|
|
13
|
-
<
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
<span>Sample #<%= @sample.id %></span>
|
|
18
|
+
</div>
|
|
19
|
+
<%= button_to "Delete Sample", catpm.sample_path(@sample),
|
|
20
|
+
method: :delete, class: "btn btn-danger",
|
|
21
|
+
data: { confirm: "Delete this sample? This cannot be undone." } %>
|
|
17
22
|
</div>
|
|
18
23
|
|
|
19
24
|
<%# ─── Request Info Bar ─── %>
|
|
@@ -17,6 +17,11 @@
|
|
|
17
17
|
end
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
+
# Sort children of each parent by timeline offset so segments appear chronologically
|
|
21
|
+
children.each_value do |kids|
|
|
22
|
+
kids.sort_by! { |i| (segments[i]["offset"] || segments[i][:offset] || 0).to_f }
|
|
23
|
+
end
|
|
24
|
+
|
|
20
25
|
depth_map = {}
|
|
21
26
|
ordered = []
|
|
22
27
|
build_order = ->(indices, depth) {
|
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
|
|
4
4
|
<%= render "catpm/shared/page_nav", active: "performance" %>
|
|
5
5
|
|
|
6
|
+
<div class="tabs">
|
|
7
|
+
<a href="<%= catpm.status_index_path(range: @range) %>" class="tab active">Active (<%= @total_endpoint_count %>)</a>
|
|
8
|
+
<a href="<%= catpm.ignored_endpoints_path %>" class="tab">Ignored (<%= @ignored_endpoints.size %>)</a>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
6
11
|
<%# ─── Time Range ─── %>
|
|
7
12
|
<div class="time-range">
|
|
8
13
|
<% (["all"] + Catpm::ApplicationHelper::RANGE_KEYS).each do |r| %>
|
|
@@ -70,7 +75,6 @@
|
|
|
70
75
|
|
|
71
76
|
<%# ─── Endpoints ─── %>
|
|
72
77
|
<h2>Endpoints <% if @total_endpoint_count > @endpoint_count %><span class="text-muted" style="font-weight:400; font-size:13px">(showing <%= @endpoint_count %> of <%= @total_endpoint_count %>)</span><% end %></h2>
|
|
73
|
-
<%= section_description("Endpoints active in the selected time range.") %>
|
|
74
78
|
<% ep_extra = { range: @range }; ep_extra[:kind] = @kind_filter if @kind_filter %>
|
|
75
79
|
<% if @endpoints.any? || @kind_filter.present? %>
|
|
76
80
|
<div class="filters">
|
|
@@ -84,6 +88,7 @@
|
|
|
84
88
|
<table id="endpoints-table">
|
|
85
89
|
<thead>
|
|
86
90
|
<tr>
|
|
91
|
+
<th></th>
|
|
87
92
|
<th>Kind</th>
|
|
88
93
|
<th><%= sort_header("Target", "target", @sort, @dir, extra_params: ep_extra) %></th>
|
|
89
94
|
<th><%= sort_header("Count", "total_count", @sort, @dir, extra_params: ep_extra) %></th>
|
|
@@ -91,18 +96,28 @@
|
|
|
91
96
|
<th><%= sort_header("Max", "max_duration", @sort, @dir, extra_params: ep_extra) %></th>
|
|
92
97
|
<th><%= sort_header("Fail", "total_failures", @sort, @dir, extra_params: ep_extra) %></th>
|
|
93
98
|
<th><%= sort_header("Last Seen", "last_seen", @sort, @dir, extra_params: ep_extra) %></th>
|
|
99
|
+
<th></th>
|
|
94
100
|
</tr>
|
|
95
101
|
</thead>
|
|
96
102
|
<tbody>
|
|
97
103
|
<% @endpoints.each do |ep| %>
|
|
104
|
+
<% ep_p = { kind: ep[:kind], target: ep[:target], operation: ep[:operation] } %>
|
|
98
105
|
<tr class="linked">
|
|
99
|
-
<td
|
|
106
|
+
<td style="position:relative; z-index:1; width:28px; padding:4px 2px; text-align:center"><button class="star-btn<%= ' pinned' if ep[:pinned] %>" data-pin-url="<%= catpm.endpoint_pin_path(ep_p) %>"><%= ep[:pinned] ? "★".html_safe : "☆".html_safe %></button></td>
|
|
107
|
+
<td><a href="<%= catpm.endpoint_path(ep_p) %>" class="row-link"><%= type_badge(ep[:kind]) %></a></td>
|
|
100
108
|
<td class="mono"><%= ep[:target] %><%= " #{ep[:operation]}" if ep[:operation].present? %></td>
|
|
101
109
|
<td><%= ep[:total_count] %></td>
|
|
102
110
|
<td class="mono"><%= format_duration(ep[:avg_duration]) %></td>
|
|
103
111
|
<td class="mono"><%= format_duration(ep[:max_duration]) %></td>
|
|
104
112
|
<td><%= ep[:total_failures] %></td>
|
|
105
113
|
<td><%= time_with_tooltip(ep[:last_seen]) %></td>
|
|
114
|
+
<td style="position:relative; z-index:2; width:28px; padding:4px 2px; text-align:center">
|
|
115
|
+
<button class="action-menu-btn">⋮</button>
|
|
116
|
+
<div class="action-menu">
|
|
117
|
+
<button data-action="ignore" data-url="<%= catpm.endpoint_ignore_path(ep_p) %>">Ignore</button>
|
|
118
|
+
<button class="menu-danger" data-action="delete" data-url="<%= catpm.endpoint_path(ep_p) %>">Delete</button>
|
|
119
|
+
</div>
|
|
120
|
+
</td>
|
|
106
121
|
</tr>
|
|
107
122
|
<% end %>
|
|
108
123
|
</tbody>
|
|
@@ -122,3 +137,4 @@
|
|
|
122
137
|
</div>
|
|
123
138
|
<% end %>
|
|
124
139
|
|
|
140
|
+
|