binocs 0.1.0 → 0.2.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.
@@ -1,7 +1,7 @@
1
1
  <div class="space-y-6">
2
2
  <!-- Back Link -->
3
3
  <div>
4
- <%= link_to binocs.requests_path, class: "inline-flex items-center text-sm text-slate-400 hover:text-white" do %>
4
+ <%= link_to binocs.requests_path, class: "inline-flex items-center text-sm text-zinc-500 hover:text-yellow-400 transition-colors" do %>
5
5
  <svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
6
6
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
7
7
  </svg>
@@ -10,7 +10,7 @@
10
10
  </div>
11
11
 
12
12
  <!-- Header -->
13
- <div class="bg-slate-800 rounded-lg p-6 border border-slate-700">
13
+ <div class="bg-zinc-900 rounded-lg p-6 border border-yellow-500/20">
14
14
  <div class="flex items-center justify-between">
15
15
  <div class="flex items-center space-x-4">
16
16
  <span class="inline-flex items-center rounded px-3 py-1.5 text-sm font-medium <%= method_badge_class(@request.method) %>">
@@ -19,45 +19,51 @@
19
19
  <span class="inline-flex items-center rounded px-3 py-1.5 text-sm font-medium <%= status_badge_class(@request.status_code) %>">
20
20
  <%= @request.status_code %>
21
21
  </span>
22
- <h1 class="text-lg font-semibold text-white break-all"><%= @request.path %></h1>
22
+ <h1 class="text-lg font-semibold text-zinc-200 break-all"><%= @request.path %></h1>
23
23
  </div>
24
24
  <div class="flex items-center space-x-2">
25
- <%= link_to "Open in Swagger", @request.swagger_url, target: "_blank", class: "rounded-md bg-indigo-600 px-3 py-2 text-sm font-medium text-white hover:bg-indigo-500" %>
26
- <%= button_to "Delete", binocs.request_path(@request.uuid), method: :delete, class: "rounded-md bg-red-600 px-3 py-2 text-sm font-medium text-white hover:bg-red-500", data: { turbo_confirm: "Are you sure?" } %>
25
+ <%= link_to binocs.lifecycle_request_path(@request.uuid), class: "rounded-md bg-yellow-500/10 text-yellow-400 ring-1 ring-yellow-500/30 px-3 py-2 text-sm font-medium hover:bg-yellow-500/20 transition-all inline-flex items-center", data: { testid: "lifecycle-link" } do %>
26
+ <svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
27
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3"></path>
28
+ </svg>
29
+ Lifecycle
30
+ <% end %>
31
+ <%= link_to "Open in Swagger", @request.swagger_url, target: "_blank", class: "rounded-md bg-yellow-500 px-3 py-2 text-sm font-medium text-black hover:bg-yellow-400 transition-colors" %>
32
+ <%= button_to "Delete", binocs.request_path(@request.uuid), method: :delete, class: "rounded-md bg-rose-500/15 text-rose-400 ring-1 ring-rose-500/20 px-3 py-2 text-sm font-medium hover:bg-rose-500/25 transition-colors", data: { turbo_confirm: "Are you sure?" } %>
27
33
  </div>
28
34
  </div>
29
35
 
30
36
  <div class="mt-4 grid grid-cols-2 gap-4 sm:grid-cols-4">
31
37
  <div>
32
- <dt class="text-sm text-slate-400">Duration</dt>
33
- <dd class="text-lg font-medium text-white"><%= @request.formatted_duration %></dd>
38
+ <dt class="text-sm text-zinc-500">Duration</dt>
39
+ <dd class="text-lg font-medium text-zinc-200"><%= @request.formatted_duration %></dd>
34
40
  </div>
35
41
  <div>
36
- <dt class="text-sm text-slate-400">Memory Delta</dt>
37
- <dd class="text-lg font-medium text-white"><%= @request.formatted_memory_delta %></dd>
42
+ <dt class="text-sm text-zinc-500">Memory Delta</dt>
43
+ <dd class="text-lg font-medium text-zinc-200"><%= @request.formatted_memory_delta %></dd>
38
44
  </div>
39
45
  <div>
40
- <dt class="text-sm text-slate-400">IP Address</dt>
41
- <dd class="text-lg font-medium text-white"><%= @request.ip_address || "N/A" %></dd>
46
+ <dt class="text-sm text-zinc-500">IP Address</dt>
47
+ <dd class="text-lg font-medium text-zinc-200"><%= @request.ip_address || "N/A" %></dd>
42
48
  </div>
43
49
  <div>
44
- <dt class="text-sm text-slate-400">Time</dt>
45
- <dd class="text-lg font-medium text-white"><%= @request.created_at.strftime("%Y-%m-%d %H:%M:%S") %></dd>
50
+ <dt class="text-sm text-zinc-500">Time</dt>
51
+ <dd class="text-lg font-medium text-zinc-200"><%= @request.created_at.strftime("%Y-%m-%d %H:%M:%S") %></dd>
46
52
  </div>
47
53
  </div>
48
54
 
49
55
  <% if @request.controller_action %>
50
- <div class="mt-4 pt-4 border-t border-slate-700">
51
- <span class="text-sm text-slate-400">Route:</span>
52
- <span class="ml-2 text-sm text-white"><%= @request.controller_action %></span>
56
+ <div class="mt-4 pt-4 border-t border-zinc-800">
57
+ <span class="text-sm text-zinc-500">Route:</span>
58
+ <span class="ml-2 text-sm text-zinc-200"><%= @request.controller_action %></span>
53
59
  </div>
54
60
  <% end %>
55
61
  </div>
56
62
 
57
63
  <!-- Exception (if present) -->
58
64
  <% if @request.has_exception? %>
59
- <div class="bg-red-900/20 rounded-lg p-6 border border-red-800">
60
- <h2 class="text-lg font-semibold text-red-400 flex items-center">
65
+ <div class="bg-rose-500/5 rounded-lg p-6 border border-rose-500/20">
66
+ <h2 class="text-lg font-semibold text-rose-400 flex items-center">
61
67
  <svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
62
68
  <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path>
63
69
  </svg>
@@ -65,42 +71,55 @@
65
71
  </h2>
66
72
  <div class="mt-4 space-y-3">
67
73
  <div>
68
- <span class="text-sm text-red-300 font-medium"><%= @request.exception["class"] %></span>
74
+ <span class="text-sm text-rose-300 font-medium"><%= @request.exception["class"] %></span>
69
75
  </div>
70
- <div class="text-sm text-red-200">
76
+ <div class="text-sm text-rose-200">
71
77
  <%= @request.exception["message"] %>
72
78
  </div>
73
79
  <% if @request.exception["backtrace"].present? %>
74
80
  <div class="mt-4">
75
- <h3 class="text-sm font-medium text-red-300 mb-2">Backtrace</h3>
76
- <pre class="bg-slate-900 rounded p-4 text-xs text-red-200 overflow-x-auto max-h-96"><%= @request.exception["backtrace"].join("\n") %></pre>
81
+ <h3 class="text-sm font-medium text-rose-300 mb-2">Backtrace</h3>
82
+ <pre class="bg-zinc-950 rounded p-4 text-xs text-rose-200 overflow-x-auto max-h-96"><%= Array(@request.exception["backtrace"]).join("\n") %></pre>
77
83
  </div>
78
84
  <% end %>
85
+ <% cause = @request.exception["cause"] %>
86
+ <% depth = 0 %>
87
+ <% while cause.present? && depth < 5 %>
88
+ <div class="mt-4 pl-4 border-l-2 border-rose-500/30">
89
+ <h3 class="text-sm font-semibold text-rose-400 mb-1">Caused by: <%= cause["class"] %></h3>
90
+ <div class="text-sm text-rose-200"><%= cause["message"] %></div>
91
+ <% if cause["backtrace"].present? %>
92
+ <pre class="mt-2 bg-zinc-950 rounded p-3 text-xs text-rose-200/70 overflow-x-auto max-h-48"><%= Array(cause["backtrace"]).join("\n") %></pre>
93
+ <% end %>
94
+ </div>
95
+ <% cause = cause["cause"] %>
96
+ <% depth += 1 %>
97
+ <% end %>
79
98
  </div>
80
99
  </div>
81
100
  <% end %>
82
101
 
83
102
  <!-- Tabs -->
84
- <div x-data="{ activeTab: 'params' }" class="bg-slate-800 rounded-lg border border-slate-700 overflow-hidden">
103
+ <div x-data="{ activeTab: 'params' }" class="bg-zinc-900 rounded-lg border border-zinc-800 overflow-hidden">
85
104
  <!-- Tab Headers -->
86
- <div class="border-b border-slate-700">
105
+ <div class="border-b border-zinc-800">
87
106
  <nav class="flex -mb-px">
88
- <button @click="activeTab = 'params'" :class="activeTab === 'params' ? 'border-indigo-500 text-indigo-400' : 'border-transparent text-slate-400 hover:text-white hover:border-slate-600'" class="px-6 py-3 border-b-2 text-sm font-medium">
107
+ <button @click="activeTab = 'params'" :class="activeTab === 'params' ? 'border-yellow-500 text-yellow-400' : 'border-transparent text-zinc-500 hover:text-zinc-200 hover:border-zinc-700'" class="px-6 py-3 border-b-2 text-sm font-medium transition-colors">
89
108
  Params
90
109
  </button>
91
- <button @click="activeTab = 'headers'" :class="activeTab === 'headers' ? 'border-indigo-500 text-indigo-400' : 'border-transparent text-slate-400 hover:text-white hover:border-slate-600'" class="px-6 py-3 border-b-2 text-sm font-medium">
110
+ <button @click="activeTab = 'headers'" :class="activeTab === 'headers' ? 'border-yellow-500 text-yellow-400' : 'border-transparent text-zinc-500 hover:text-zinc-200 hover:border-zinc-700'" class="px-6 py-3 border-b-2 text-sm font-medium transition-colors">
92
111
  Headers
93
112
  </button>
94
- <button @click="activeTab = 'body'" :class="activeTab === 'body' ? 'border-indigo-500 text-indigo-400' : 'border-transparent text-slate-400 hover:text-white hover:border-slate-600'" class="px-6 py-3 border-b-2 text-sm font-medium">
113
+ <button @click="activeTab = 'body'" :class="activeTab === 'body' ? 'border-yellow-500 text-yellow-400' : 'border-transparent text-zinc-500 hover:text-zinc-200 hover:border-zinc-700'" class="px-6 py-3 border-b-2 text-sm font-medium transition-colors">
95
114
  Body
96
115
  </button>
97
- <button @click="activeTab = 'response'" :class="activeTab === 'response' ? 'border-indigo-500 text-indigo-400' : 'border-transparent text-slate-400 hover:text-white hover:border-slate-600'" class="px-6 py-3 border-b-2 text-sm font-medium">
116
+ <button @click="activeTab = 'response'" :class="activeTab === 'response' ? 'border-yellow-500 text-yellow-400' : 'border-transparent text-zinc-500 hover:text-zinc-200 hover:border-zinc-700'" class="px-6 py-3 border-b-2 text-sm font-medium transition-colors">
98
117
  Response
99
118
  </button>
100
- <button @click="activeTab = 'logs'" :class="activeTab === 'logs' ? 'border-indigo-500 text-indigo-400' : 'border-transparent text-slate-400 hover:text-white hover:border-slate-600'" class="px-6 py-3 border-b-2 text-sm font-medium">
119
+ <button @click="activeTab = 'logs'" :class="activeTab === 'logs' ? 'border-yellow-500 text-yellow-400' : 'border-transparent text-zinc-500 hover:text-zinc-200 hover:border-zinc-700'" class="px-6 py-3 border-b-2 text-sm font-medium transition-colors">
101
120
  Logs
102
121
  <% if @request.logs.present? && @request.logs.any? %>
103
- <span class="ml-2 inline-flex items-center rounded-full bg-slate-700 px-2 py-0.5 text-xs"><%= @request.logs.size %></span>
122
+ <span class="ml-2 inline-flex items-center rounded-full bg-zinc-800 px-2 py-0.5 text-xs text-zinc-400"><%= @request.logs.size %></span>
104
123
  <% end %>
105
124
  </button>
106
125
  </nav>
@@ -114,13 +133,13 @@
114
133
  <div class="space-y-2">
115
134
  <% @request.params.each do |key, value| %>
116
135
  <div class="flex">
117
- <span class="text-sm font-medium text-indigo-400 w-48 flex-shrink-0"><%= key %></span>
118
- <span class="text-sm text-white break-all"><%= format_value(value) %></span>
136
+ <span class="text-sm font-medium text-yellow-400 w-48 flex-shrink-0"><%= key %></span>
137
+ <span class="text-sm text-zinc-200 break-all"><%= format_value(value) %></span>
119
138
  </div>
120
139
  <% end %>
121
140
  </div>
122
141
  <% else %>
123
- <p class="text-sm text-slate-400">No parameters</p>
142
+ <p class="text-sm text-zinc-500">No parameters</p>
124
143
  <% end %>
125
144
  </div>
126
145
 
@@ -128,34 +147,34 @@
128
147
  <div x-show="activeTab === 'headers'" x-cloak>
129
148
  <div class="space-y-6">
130
149
  <div>
131
- <h3 class="text-sm font-medium text-slate-300 mb-3">Request Headers</h3>
150
+ <h3 class="text-sm font-medium text-zinc-300 mb-3">Request Headers</h3>
132
151
  <% if @request.request_headers.present? && @request.request_headers.any? %>
133
152
  <div class="space-y-2">
134
153
  <% @request.request_headers.each do |key, value| %>
135
154
  <div class="flex">
136
- <span class="text-sm font-medium text-indigo-400 w-48 flex-shrink-0"><%= key %></span>
137
- <span class="text-sm text-white break-all"><%= value %></span>
155
+ <span class="text-sm font-medium text-yellow-400 w-48 flex-shrink-0"><%= key %></span>
156
+ <span class="text-sm text-zinc-200 break-all"><%= value %></span>
138
157
  </div>
139
158
  <% end %>
140
159
  </div>
141
160
  <% else %>
142
- <p class="text-sm text-slate-400">No request headers</p>
161
+ <p class="text-sm text-zinc-500">No request headers</p>
143
162
  <% end %>
144
163
  </div>
145
164
 
146
165
  <div>
147
- <h3 class="text-sm font-medium text-slate-300 mb-3">Response Headers</h3>
166
+ <h3 class="text-sm font-medium text-zinc-300 mb-3">Response Headers</h3>
148
167
  <% if @request.response_headers.present? && @request.response_headers.any? %>
149
168
  <div class="space-y-2">
150
169
  <% @request.response_headers.each do |key, value| %>
151
170
  <div class="flex">
152
- <span class="text-sm font-medium text-indigo-400 w-48 flex-shrink-0"><%= key %></span>
153
- <span class="text-sm text-white break-all"><%= value %></span>
171
+ <span class="text-sm font-medium text-yellow-400 w-48 flex-shrink-0"><%= key %></span>
172
+ <span class="text-sm text-zinc-200 break-all"><%= value %></span>
154
173
  </div>
155
174
  <% end %>
156
175
  </div>
157
176
  <% else %>
158
- <p class="text-sm text-slate-400">No response headers</p>
177
+ <p class="text-sm text-zinc-500">No response headers</p>
159
178
  <% end %>
160
179
  </div>
161
180
  </div>
@@ -163,65 +182,53 @@
163
182
 
164
183
  <!-- Body Tab -->
165
184
  <div x-show="activeTab === 'body'" x-cloak>
166
- <h3 class="text-sm font-medium text-slate-300 mb-3">Request Body</h3>
185
+ <h3 class="text-sm font-medium text-zinc-300 mb-3">Request Body</h3>
167
186
  <% if @request.request_body.present? %>
168
- <pre class="bg-slate-900 rounded p-4 text-sm text-white overflow-x-auto max-h-96"><%= format_body(@request.request_body) %></pre>
187
+ <pre class="bg-zinc-950 rounded p-4 text-sm text-zinc-200 overflow-x-auto max-h-96"><%= format_body(@request.request_body) %></pre>
169
188
  <% else %>
170
- <p class="text-sm text-slate-400">No request body</p>
189
+ <p class="text-sm text-zinc-500">No request body</p>
171
190
  <% end %>
172
191
  </div>
173
192
 
174
193
  <!-- Response Tab -->
175
194
  <div x-show="activeTab === 'response'" x-cloak>
176
- <h3 class="text-sm font-medium text-slate-300 mb-3">Response Body</h3>
195
+ <h3 class="text-sm font-medium text-zinc-300 mb-3">Response Body</h3>
177
196
  <% if @request.response_body.present? %>
178
- <pre class="bg-slate-900 rounded p-4 text-sm text-white overflow-x-auto max-h-96"><%= format_body(@request.response_body) %></pre>
197
+ <pre class="bg-zinc-950 rounded p-4 text-sm text-zinc-200 overflow-x-auto max-h-96"><%= format_body(@request.response_body) %></pre>
179
198
  <% else %>
180
- <p class="text-sm text-slate-400">No response body captured</p>
199
+ <p class="text-sm text-zinc-500">No response body captured</p>
181
200
  <% end %>
182
201
  </div>
183
202
 
184
203
  <!-- Logs Tab -->
185
204
  <div x-show="activeTab === 'logs'" x-cloak>
186
205
  <% if @request.logs.present? && @request.logs.any? %>
206
+ <!-- Log type filter -->
207
+ <div class="mb-4 flex items-center space-x-2 flex-wrap" x-data="{ logFilter: 'all' }">
208
+ <span class="text-xs text-zinc-600 mr-1">Filter:</span>
209
+ <button @click="logFilter = 'all'" :class="logFilter === 'all' ? 'bg-yellow-500/15 text-yellow-400 ring-1 ring-yellow-500/30' : 'bg-zinc-800 text-zinc-500 hover:text-zinc-200'" class="rounded px-2 py-1 text-xs font-medium transition-colors">All (<%= @request.logs.size %>)</button>
210
+ <% log_types = @request.logs.map { |l| l["type"] }.tally.sort_by { |_, c| -c } %>
211
+ <% log_types.each do |type, count| %>
212
+ <button @click="logFilter = '<%= type %>'" :class="logFilter === '<%= type %>' ? 'bg-yellow-500/15 text-yellow-400 ring-1 ring-yellow-500/30' : 'bg-zinc-800 text-zinc-500 hover:text-zinc-200'" class="rounded px-2 py-1 text-xs font-medium transition-colors"><%= type %> (<%= count %>)</button>
213
+ <% end %>
214
+ </div>
187
215
  <div class="space-y-3">
188
216
  <% @request.logs.each do |log| %>
189
- <div class="bg-slate-900 rounded p-4">
190
- <div class="flex items-center justify-between mb-2">
191
- <span class="text-xs font-medium text-indigo-400 uppercase"><%= log["type"] %></span>
192
- <span class="text-xs text-slate-500"><%= log["timestamp"] %></span>
193
- </div>
194
- <% if log["type"] == "controller" %>
195
- <div class="text-sm text-white">
196
- <span class="text-slate-400">Controller:</span> <%= log["controller"] %>#<%= log["action"] %>
197
- <% if log["view_runtime"] %>
198
- <span class="ml-4 text-slate-400">View:</span> <%= log["view_runtime"] %>ms
199
- <% end %>
200
- <% if log["db_runtime"] %>
201
- <span class="ml-4 text-slate-400">DB:</span> <%= log["db_runtime"] %>ms
202
- <% end %>
203
- </div>
204
- <% elsif log["type"] == "redirect" %>
205
- <div class="text-sm text-white">
206
- <span class="text-slate-400">Redirect to:</span> <%= log["location"] %>
207
- <span class="ml-4 text-slate-400">Status:</span> <%= log["status"] %>
208
- </div>
209
- <% else %>
210
- <pre class="text-sm text-white"><%= format_value(log) %></pre>
211
- <% end %>
217
+ <div x-show="logFilter === 'all' || logFilter === '<%= log["type"] %>'">
218
+ <%= render partial: "binocs/requests/log_entry", locals: { log: log } %>
212
219
  </div>
213
220
  <% end %>
214
221
  </div>
215
222
  <% else %>
216
- <p class="text-sm text-slate-400">No logs captured</p>
223
+ <p class="text-sm text-zinc-500">No logs captured</p>
217
224
  <% end %>
218
225
  </div>
219
226
  </div>
220
227
  </div>
221
228
 
222
229
  <!-- Full URL -->
223
- <div class="bg-slate-800 rounded-lg p-6 border border-slate-700">
224
- <h2 class="text-sm font-medium text-slate-400 mb-2">Full URL</h2>
225
- <p class="text-sm text-white break-all font-mono"><%= @request.full_url %></p>
230
+ <div class="bg-zinc-900 rounded-lg p-6 border border-zinc-800">
231
+ <h2 class="text-sm font-medium text-zinc-500 mb-2">Full URL</h2>
232
+ <p class="text-sm text-zinc-200 break-all font-mono"><%= @request.full_url %></p>
226
233
  </div>
227
234
  </div>
@@ -1,5 +1,5 @@
1
1
  <!DOCTYPE html>
2
- <html lang="en" class="h-full bg-gray-900">
2
+ <html lang="en" class="h-full bg-zinc-950">
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
@@ -16,10 +16,11 @@
16
16
  extend: {
17
17
  colors: {
18
18
  binocs: {
19
- bg: '#0f172a',
20
- card: '#1e293b',
21
- border: '#334155',
22
- accent: '#6366f1'
19
+ bg: '#09090b',
20
+ card: '#18181b',
21
+ border: '#27272a',
22
+ accent: '#eab308',
23
+ gold: '#ca8a04'
23
24
  }
24
25
  }
25
26
  }
@@ -33,20 +34,36 @@
33
34
  <body class="h-full">
34
35
  <div class="min-h-full">
35
36
  <!-- Navigation -->
36
- <nav class="bg-slate-800 border-b border-slate-700">
37
+ <nav class="bg-zinc-950 border-b border-yellow-500/20">
37
38
  <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
38
39
  <div class="flex h-16 items-center justify-between">
39
40
  <div class="flex items-center">
40
- <div class="flex-shrink-0">
41
- <span class="text-2xl font-bold text-indigo-400">Binocs</span>
42
- <span class="ml-2 text-sm text-slate-400">Request Monitor</span>
41
+ <div class="flex-shrink-0 flex items-center">
42
+ <!-- Binoculars icon inspired by the Binocs brand icon -->
43
+ <svg class="w-7 h-7 mr-2" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
44
+ <circle cx="8" cy="14" r="5" stroke="#eab308" stroke-width="1.5" fill="#eab308" fill-opacity="0.15"/>
45
+ <circle cx="16" cy="14" r="5" stroke="#eab308" stroke-width="1.5" fill="#eab308" fill-opacity="0.15"/>
46
+ <circle cx="8" cy="14" r="2.5" fill="#18181b" stroke="#fefce8" stroke-width="0.75"/>
47
+ <circle cx="16" cy="14" r="2.5" fill="#18181b" stroke="#fefce8" stroke-width="0.75"/>
48
+ <rect x="11" y="12" width="2" height="4" rx="1" fill="#eab308"/>
49
+ <path d="M6 9 L8 11" stroke="#eab308" stroke-width="1.5" stroke-linecap="round"/>
50
+ <path d="M18 9 L16 11" stroke="#eab308" stroke-width="1.5" stroke-linecap="round"/>
51
+ <!-- Cyan accent dots from brand icon -->
52
+ <circle cx="8" cy="14" r="0.75" fill="#22d3ee"/>
53
+ <circle cx="16" cy="14" r="0.75" fill="#22d3ee"/>
54
+ </svg>
55
+ <span class="text-2xl font-bold text-yellow-500">Binocs</span>
56
+ <span class="ml-2 text-sm text-zinc-500">Request Monitor</span>
43
57
  </div>
44
58
  <div class="ml-10 flex items-baseline space-x-4">
45
- <%= link_to "Requests", binocs.requests_path, class: "rounded-md px-3 py-2 text-sm font-medium #{current_page?(binocs.requests_path) ? 'bg-slate-900 text-white' : 'text-slate-300 hover:bg-slate-700 hover:text-white'}" %>
59
+ <%= link_to "Requests", binocs.requests_path, class: "rounded-md px-3 py-2 text-sm font-medium transition-all #{current_page?(binocs.requests_path) ? 'bg-yellow-500/10 text-yellow-400 ring-1 ring-yellow-500/30' : 'text-zinc-400 hover:bg-zinc-800 hover:text-yellow-400'}" %>
60
+ <%= link_to "Sequence", binocs.sequence_requests_path, class: "rounded-md px-3 py-2 text-sm font-medium transition-all #{current_page?(binocs.sequence_requests_path) ? 'bg-yellow-500/10 text-yellow-400 ring-1 ring-yellow-500/30' : 'text-zinc-400 hover:bg-zinc-800 hover:text-yellow-400'}" %>
61
+ <%= link_to "Heatmap", binocs.heatmap_requests_path, class: "rounded-md px-3 py-2 text-sm font-medium transition-all #{current_page?(binocs.heatmap_requests_path) ? 'bg-yellow-500/10 text-yellow-400 ring-1 ring-yellow-500/30' : 'text-zinc-400 hover:bg-zinc-800 hover:text-yellow-400'}" %>
62
+ <%= link_to "Analytics", binocs.analytics_requests_path, class: "rounded-md px-3 py-2 text-sm font-medium transition-all #{current_page?(binocs.analytics_requests_path) ? 'bg-yellow-500/10 text-yellow-400 ring-1 ring-yellow-500/30' : 'text-zinc-400 hover:bg-zinc-800 hover:text-yellow-400'}" %>
46
63
  </div>
47
64
  </div>
48
65
  <div class="flex items-center space-x-4">
49
- <span class="text-xs text-slate-500">
66
+ <span class="text-xs text-zinc-600">
50
67
  Rails <%= Rails::VERSION::STRING %> |
51
68
  Ruby <%= RUBY_VERSION %> |
52
69
  <%= Rails.env %>
@@ -57,7 +74,7 @@
57
74
  </nav>
58
75
 
59
76
  <!-- Authentication Required Banner (hidden by default, shown by JS when WebSocket fails) -->
60
- <div id="binocs-auth-banner" class="hidden bg-amber-900/50 border-b border-amber-700">
77
+ <div id="binocs-auth-banner" class="hidden bg-amber-900/30 border-b border-amber-700/50">
61
78
  <div class="mx-auto max-w-7xl px-4 py-3 sm:px-6 lg:px-8">
62
79
  <div class="flex items-center justify-between flex-wrap gap-2">
63
80
  <div class="flex items-center">
@@ -71,10 +88,10 @@
71
88
  </div>
72
89
  <div class="flex gap-2">
73
90
  <% login_url = "#{Binocs.configuration.resolved_login_path}?redirect_to=#{ERB::Util.url_encode(request.fullpath)}" rescue Binocs.configuration.resolved_login_path %>
74
- <a href="<%= login_url %>" class="rounded-md bg-amber-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-amber-500">
91
+ <a href="<%= login_url %>" class="rounded-md bg-yellow-500 px-3 py-1.5 text-sm font-medium text-black hover:bg-yellow-400 transition-colors">
75
92
  Sign in
76
93
  </a>
77
- <button onclick="document.getElementById('binocs-auth-banner').classList.add('hidden')" class="rounded-md bg-amber-800 px-3 py-1.5 text-sm font-medium text-amber-200 hover:bg-amber-700">
94
+ <button onclick="document.getElementById('binocs-auth-banner').classList.add('hidden')" class="rounded-md bg-zinc-800 px-3 py-1.5 text-sm font-medium text-zinc-300 hover:bg-zinc-700 transition-colors">
78
95
  Dismiss
79
96
  </button>
80
97
  </div>
@@ -84,22 +101,22 @@
84
101
 
85
102
  <!-- Flash Messages -->
86
103
  <% if notice %>
87
- <div class="bg-green-900/50 border-b border-green-800">
104
+ <div class="bg-emerald-900/30 border-b border-emerald-800/50">
88
105
  <div class="mx-auto max-w-7xl px-4 py-2 sm:px-6 lg:px-8">
89
- <p class="text-sm text-green-300"><%= notice %></p>
106
+ <p class="text-sm text-emerald-300"><%= notice %></p>
90
107
  </div>
91
108
  </div>
92
109
  <% end %>
93
110
  <% if alert %>
94
- <div class="bg-red-900/50 border-b border-red-800">
111
+ <div class="bg-rose-900/30 border-b border-rose-800/50">
95
112
  <div class="mx-auto max-w-7xl px-4 py-2 sm:px-6 lg:px-8">
96
- <p class="text-sm text-red-300"><%= alert %></p>
113
+ <p class="text-sm text-rose-300"><%= alert %></p>
97
114
  </div>
98
115
  </div>
99
116
  <% end %>
100
117
 
101
118
  <!-- Main Content -->
102
- <main class="bg-slate-900 min-h-screen">
119
+ <main class="bg-zinc-950 min-h-screen">
103
120
  <div class="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
104
121
  <%= yield %>
105
122
  </div>
data/config/routes.rb CHANGED
@@ -4,8 +4,15 @@ Binocs::Engine.routes.draw do
4
4
  root to: "requests#index"
5
5
 
6
6
  resources :requests, only: [:index, :show, :destroy] do
7
+ member do
8
+ get :lifecycle
9
+ get :raw
10
+ end
7
11
  collection do
8
12
  delete :clear
13
+ get :sequence
14
+ get :heatmap
15
+ get :analytics
9
16
  end
10
17
  end
11
18
  end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddClientIdentifierToBinocsRequests < ActiveRecord::Migration[7.0]
4
+ def change
5
+ add_column :binocs_requests, :client_identifier, :string
6
+ add_index :binocs_requests, :client_identifier
7
+ end
8
+ end
data/lib/binocs/engine.rb CHANGED
@@ -25,6 +25,15 @@ module Binocs
25
25
 
26
26
  require_relative "log_subscriber"
27
27
  Binocs::LogSubscriber.attach_to :action_controller
28
+ Binocs::SqlLogSubscriber.attach_to :active_record
29
+ Binocs::ViewLogSubscriber.attach_to :action_view
30
+ end
31
+
32
+ initializer "binocs.log_interceptor", after: :initialize_logger do
33
+ next unless Binocs.enabled?
34
+
35
+ require_relative "log_subscriber"
36
+ Rails.logger = Binocs::LogInterceptor.new(Rails.logger)
28
37
  end
29
38
 
30
39
  initializer "binocs.assets" do |app|
@@ -20,6 +20,26 @@ module Binocs
20
20
  duration: event.duration.round(2),
21
21
  timestamp: Time.current.iso8601
22
22
  }
23
+
24
+ # Capture exception info from the payload if present
25
+ if payload[:exception_object]
26
+ ex = payload[:exception_object]
27
+ Thread.current[:binocs_logs] << {
28
+ type: "exception",
29
+ class: ex.class.name,
30
+ message: ex.message,
31
+ backtrace: ex.backtrace&.first(30),
32
+ cause: exception_cause_chain(ex),
33
+ timestamp: Time.current.iso8601
34
+ }
35
+ elsif payload[:exception]
36
+ Thread.current[:binocs_logs] << {
37
+ type: "exception",
38
+ class: payload[:exception].first,
39
+ message: payload[:exception].second,
40
+ timestamp: Time.current.iso8601
41
+ }
42
+ end
23
43
  end
24
44
 
25
45
  def halted_callback(event)
@@ -52,5 +72,149 @@ module Binocs
52
72
  timestamp: Time.current.iso8601
53
73
  }
54
74
  end
75
+
76
+ private
77
+
78
+ def exception_cause_chain(ex, depth = 0)
79
+ return nil if ex.cause.nil? || depth >= 5
80
+
81
+ cause = ex.cause
82
+ {
83
+ class: cause.class.name,
84
+ message: cause.message,
85
+ backtrace: cause.backtrace&.first(10),
86
+ cause: exception_cause_chain(cause, depth + 1)
87
+ }
88
+ end
89
+ end
90
+
91
+ # Captures ActiveRecord SQL queries during a request
92
+ class SqlLogSubscriber < ActiveSupport::LogSubscriber
93
+ # Only capture queries that are meaningful for debugging
94
+ IGNORED_QUERIES = %w[SCHEMA EXPLAIN].freeze
95
+
96
+ def sql(event)
97
+ return unless Thread.current[:binocs_logs]
98
+
99
+ payload = event.payload
100
+ return if IGNORED_QUERIES.include?(payload[:name])
101
+ return if payload[:name] == "CACHE"
102
+
103
+ entry = {
104
+ type: "sql",
105
+ name: payload[:name] || "SQL",
106
+ sql: truncate_sql(payload[:sql]),
107
+ duration: event.duration.round(2),
108
+ timestamp: Time.current.iso8601
109
+ }
110
+
111
+ # Flag queries that are likely part of an error
112
+ if payload[:exception]
113
+ entry[:error] = true
114
+ entry[:exception_class] = payload[:exception].first
115
+ entry[:exception_message] = payload[:exception].second
116
+ end
117
+
118
+ Thread.current[:binocs_logs] << entry
119
+ end
120
+
121
+ private
122
+
123
+ def truncate_sql(sql)
124
+ return nil if sql.nil?
125
+
126
+ sql = sql.squish
127
+ sql.length > 2000 ? "#{sql[0, 2000]}..." : sql
128
+ end
129
+ end
130
+
131
+ # Captures ActionView template and partial renders
132
+ class ViewLogSubscriber < ActiveSupport::LogSubscriber
133
+ def render_template(event)
134
+ record_render(event, "template")
135
+ end
136
+
137
+ def render_partial(event)
138
+ record_render(event, "partial")
139
+ end
140
+
141
+ def render_layout(event)
142
+ record_render(event, "layout")
143
+ end
144
+
145
+ private
146
+
147
+ def record_render(event, render_type)
148
+ return unless Thread.current[:binocs_logs]
149
+
150
+ payload = event.payload
151
+ identifier = payload[:identifier]
152
+ # Make the path relative to Rails root for readability
153
+ if identifier && defined?(Rails.root)
154
+ identifier = identifier.sub("#{Rails.root}/", "")
155
+ end
156
+
157
+ entry = {
158
+ type: "render",
159
+ render_type: render_type,
160
+ identifier: identifier,
161
+ duration: event.duration.round(2),
162
+ timestamp: Time.current.iso8601
163
+ }
164
+
165
+ if payload[:exception]
166
+ entry[:error] = true
167
+ entry[:exception_class] = payload[:exception].first
168
+ entry[:exception_message] = payload[:exception].second
169
+ end
170
+
171
+ Thread.current[:binocs_logs] << entry
172
+ end
173
+ end
174
+
175
+ # Intercepts Rails.logger output during a request to capture log lines
176
+ class LogInterceptor < Logger
177
+ def initialize(original_logger)
178
+ @original_logger = original_logger
179
+ super(nil)
180
+ self.level = original_logger.level
181
+ self.formatter = original_logger.formatter
182
+ end
183
+
184
+ def add(severity, message = nil, progname = nil, &block)
185
+ # Always pass through to original logger
186
+ @original_logger.add(severity, message, progname, &block)
187
+
188
+ # Capture to binocs logs if we're in a request context and severity >= WARN
189
+ if Thread.current[:binocs_logs] && severity && severity >= Logger::WARN
190
+ msg = message || (block ? block.call : progname)
191
+ return true if msg.nil? || msg.to_s.strip.empty?
192
+
193
+ level_name = case severity
194
+ when Logger::WARN then "warn"
195
+ when Logger::ERROR then "error"
196
+ when Logger::FATAL then "fatal"
197
+ else "unknown"
198
+ end
199
+
200
+ Thread.current[:binocs_logs] << {
201
+ type: "log",
202
+ level: level_name,
203
+ message: msg.to_s[0, 4000],
204
+ timestamp: Time.current.iso8601
205
+ }
206
+ end
207
+
208
+ true
209
+ end
210
+
211
+ # Delegate everything else to the original logger
212
+ def method_missing(method, *args, &block)
213
+ @original_logger.send(method, *args, &block)
214
+ end
215
+
216
+ def respond_to_missing?(method, include_private = false)
217
+ @original_logger.respond_to?(method, include_private) || super
218
+ end
55
219
  end
56
220
  end