railscope 0.1.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 +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +227 -0
- data/Rakefile +12 -0
- data/app/assets/stylesheets/railscope/application.css +504 -0
- data/app/controllers/railscope/api/entries_controller.rb +103 -0
- data/app/controllers/railscope/application_controller.rb +12 -0
- data/app/controllers/railscope/dashboard_controller.rb +33 -0
- data/app/controllers/railscope/entries_controller.rb +29 -0
- data/app/helpers/railscope/dashboard_helper.rb +157 -0
- data/app/jobs/railscope/application_job.rb +6 -0
- data/app/jobs/railscope/purge_job.rb +15 -0
- data/app/models/railscope/application_record.rb +12 -0
- data/app/models/railscope/entry.rb +51 -0
- data/app/views/layouts/railscope/application.html.erb +14 -0
- data/app/views/railscope/application/index.html.erb +1 -0
- data/app/views/railscope/dashboard/index.html.erb +70 -0
- data/app/views/railscope/entries/show.html.erb +93 -0
- data/client/.gitignore +1 -0
- data/client/index.html +12 -0
- data/client/package-lock.json +2735 -0
- data/client/package.json +28 -0
- data/client/postcss.config.js +6 -0
- data/client/src/App.tsx +60 -0
- data/client/src/api/client.ts +25 -0
- data/client/src/api/entries.ts +36 -0
- data/client/src/components/Layout.tsx +17 -0
- data/client/src/components/PlaceholderPage.tsx +32 -0
- data/client/src/components/Sidebar.tsx +198 -0
- data/client/src/components/ui/Badge.tsx +67 -0
- data/client/src/components/ui/Card.tsx +38 -0
- data/client/src/components/ui/JsonViewer.tsx +80 -0
- data/client/src/components/ui/Pagination.tsx +45 -0
- data/client/src/components/ui/SearchInput.tsx +70 -0
- data/client/src/components/ui/Table.tsx +68 -0
- data/client/src/index.css +28 -0
- data/client/src/lib/hooks.ts +37 -0
- data/client/src/lib/types.ts +61 -0
- data/client/src/lib/utils.ts +38 -0
- data/client/src/main.tsx +13 -0
- data/client/src/screens/cache/Index.tsx +15 -0
- data/client/src/screens/client-requests/Index.tsx +15 -0
- data/client/src/screens/commands/Index.tsx +133 -0
- data/client/src/screens/commands/Show.tsx +395 -0
- data/client/src/screens/dumps/Index.tsx +15 -0
- data/client/src/screens/events/Index.tsx +15 -0
- data/client/src/screens/exceptions/Index.tsx +155 -0
- data/client/src/screens/exceptions/Show.tsx +480 -0
- data/client/src/screens/gates/Index.tsx +15 -0
- data/client/src/screens/jobs/Index.tsx +153 -0
- data/client/src/screens/jobs/Show.tsx +529 -0
- data/client/src/screens/logs/Index.tsx +15 -0
- data/client/src/screens/mail/Index.tsx +15 -0
- data/client/src/screens/models/Index.tsx +15 -0
- data/client/src/screens/notifications/Index.tsx +15 -0
- data/client/src/screens/queries/Index.tsx +159 -0
- data/client/src/screens/queries/Show.tsx +346 -0
- data/client/src/screens/redis/Index.tsx +15 -0
- data/client/src/screens/requests/Index.tsx +123 -0
- data/client/src/screens/requests/Show.tsx +395 -0
- data/client/src/screens/schedule/Index.tsx +15 -0
- data/client/src/screens/views/Index.tsx +141 -0
- data/client/src/screens/views/Show.tsx +337 -0
- data/client/tailwind.config.js +22 -0
- data/client/tsconfig.json +25 -0
- data/client/tsconfig.node.json +10 -0
- data/client/vite.config.ts +37 -0
- data/config/routes.rb +17 -0
- data/db/migrate/20260131023242_create_railscope_entries.rb +41 -0
- data/lib/generators/railscope/install_generator.rb +33 -0
- data/lib/generators/railscope/templates/initializer.rb +34 -0
- data/lib/railscope/context.rb +91 -0
- data/lib/railscope/engine.rb +85 -0
- data/lib/railscope/entry_data.rb +112 -0
- data/lib/railscope/filter.rb +113 -0
- data/lib/railscope/middleware.rb +162 -0
- data/lib/railscope/storage/base.rb +90 -0
- data/lib/railscope/storage/database.rb +83 -0
- data/lib/railscope/storage/redis_storage.rb +314 -0
- data/lib/railscope/subscribers/base_subscriber.rb +52 -0
- data/lib/railscope/subscribers/command_subscriber.rb +237 -0
- data/lib/railscope/subscribers/exception_subscriber.rb +113 -0
- data/lib/railscope/subscribers/job_subscriber.rb +249 -0
- data/lib/railscope/subscribers/query_subscriber.rb +130 -0
- data/lib/railscope/subscribers/request_subscriber.rb +121 -0
- data/lib/railscope/subscribers/view_subscriber.rb +201 -0
- data/lib/railscope/version.rb +5 -0
- data/lib/railscope.rb +145 -0
- data/lib/tasks/railscope_sample.rake +30 -0
- data/public/railscope/assets/app.css +1 -0
- data/public/railscope/assets/app.js +70 -0
- data/public/railscope/assets/index.html +13 -0
- data/sig/railscope.rbs +4 -0
- metadata +157 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Railscope
|
|
4
|
+
module DashboardHelper
|
|
5
|
+
def entry_title(entry)
|
|
6
|
+
case entry.entry_type
|
|
7
|
+
when "request"
|
|
8
|
+
"#{entry.payload["method"]} #{entry.payload["path"]}"
|
|
9
|
+
when "query"
|
|
10
|
+
entry.payload["name"] || entry.payload["sql"].to_s.truncate(50)
|
|
11
|
+
when "exception"
|
|
12
|
+
entry.payload["class"]
|
|
13
|
+
when "job_enqueue", "job_perform"
|
|
14
|
+
entry.payload["job_class"]
|
|
15
|
+
else
|
|
16
|
+
entry.entry_type.titleize
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def timeline_summary(entry)
|
|
21
|
+
case entry.entry_type
|
|
22
|
+
when "request"
|
|
23
|
+
"#{entry.payload["method"]} #{entry.payload["path"]}"
|
|
24
|
+
when "query"
|
|
25
|
+
entry.payload["sql"].to_s.truncate(40)
|
|
26
|
+
when "exception"
|
|
27
|
+
entry.payload["class"]
|
|
28
|
+
when "job_enqueue", "job_perform"
|
|
29
|
+
entry.payload["job_class"]
|
|
30
|
+
else
|
|
31
|
+
entry.entry_type
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def render_json(hash, indent: 0)
|
|
36
|
+
return content_tag(:span, "null", class: "json-null") if hash.nil?
|
|
37
|
+
|
|
38
|
+
content_tag(:div, class: "json-object") do
|
|
39
|
+
safe_join(hash.map do |key, value|
|
|
40
|
+
content_tag(:div, class: "json-row", style: "padding-left: #{indent * 16}px") do
|
|
41
|
+
key_tag = content_tag(:span, "#{key}:", class: "json-key")
|
|
42
|
+
value_tag = render_json_value(value, indent: indent)
|
|
43
|
+
safe_join([key_tag, value_tag], " ")
|
|
44
|
+
end
|
|
45
|
+
end)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def render_json_value(value, indent: 0)
|
|
50
|
+
case value
|
|
51
|
+
when nil
|
|
52
|
+
content_tag(:span, "null", class: "json-null")
|
|
53
|
+
when true, false
|
|
54
|
+
content_tag(:span, value.to_s, class: "json-boolean")
|
|
55
|
+
when Numeric
|
|
56
|
+
content_tag(:span, value.to_s, class: "json-number")
|
|
57
|
+
when String
|
|
58
|
+
if value.length > 100
|
|
59
|
+
content_tag(:span, class: "json-string long") do
|
|
60
|
+
content_tag(:code, value)
|
|
61
|
+
end
|
|
62
|
+
else
|
|
63
|
+
content_tag(:span, "\"#{value}\"", class: "json-string")
|
|
64
|
+
end
|
|
65
|
+
when Array
|
|
66
|
+
if value.empty?
|
|
67
|
+
content_tag(:span, "[]", class: "json-array-empty")
|
|
68
|
+
else
|
|
69
|
+
content_tag(:div, class: "json-array") do
|
|
70
|
+
safe_join([
|
|
71
|
+
content_tag(:span, "[", class: "json-bracket"),
|
|
72
|
+
content_tag(:div, class: "json-array-items") do
|
|
73
|
+
safe_join(value.map { |v| render_json_value(v, indent: indent + 1) })
|
|
74
|
+
end,
|
|
75
|
+
content_tag(:span, "]", class: "json-bracket")
|
|
76
|
+
])
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
when Hash
|
|
80
|
+
if value.empty?
|
|
81
|
+
content_tag(:span, "{}", class: "json-object-empty")
|
|
82
|
+
else
|
|
83
|
+
render_json(value, indent: indent + 1)
|
|
84
|
+
end
|
|
85
|
+
else
|
|
86
|
+
content_tag(:span, value.to_s, class: "json-string")
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def render_entry_summary(entry)
|
|
91
|
+
case entry.entry_type
|
|
92
|
+
when "request"
|
|
93
|
+
render_request_summary(entry.payload)
|
|
94
|
+
when "query"
|
|
95
|
+
render_query_summary(entry.payload)
|
|
96
|
+
when "exception"
|
|
97
|
+
render_exception_summary(entry.payload)
|
|
98
|
+
when "job_enqueue", "job_perform"
|
|
99
|
+
render_job_summary(entry.payload)
|
|
100
|
+
else
|
|
101
|
+
entry.payload.to_json.truncate(100)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
def render_request_summary(payload)
|
|
108
|
+
status_class = case payload["status"].to_i
|
|
109
|
+
when 200..299 then "success"
|
|
110
|
+
when 300..399 then "redirect"
|
|
111
|
+
when 400..499 then "client-error"
|
|
112
|
+
when 500..599 then "server-error"
|
|
113
|
+
else "unknown"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
content_tag(:span, class: "request-summary") do
|
|
117
|
+
safe_join([
|
|
118
|
+
content_tag(:span, payload["method"], class: "method"),
|
|
119
|
+
content_tag(:span, payload["path"], class: "path"),
|
|
120
|
+
content_tag(:span, payload["status"], class: "status #{status_class}"),
|
|
121
|
+
content_tag(:span, "#{payload["duration"]}ms", class: "duration")
|
|
122
|
+
], " ")
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def render_query_summary(payload)
|
|
127
|
+
sql = payload["sql"].to_s.truncate(80)
|
|
128
|
+
duration = payload["duration"]
|
|
129
|
+
|
|
130
|
+
content_tag(:span, class: "query-summary") do
|
|
131
|
+
safe_join([
|
|
132
|
+
content_tag(:code, sql, class: "sql"),
|
|
133
|
+
content_tag(:span, "#{duration}ms", class: "duration")
|
|
134
|
+
], " ")
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def render_exception_summary(payload)
|
|
139
|
+
content_tag(:span, class: "exception-summary") do
|
|
140
|
+
safe_join([
|
|
141
|
+
content_tag(:span, payload["class"], class: "exception-class"),
|
|
142
|
+
content_tag(:span, payload["message"].to_s.truncate(60), class: "exception-message")
|
|
143
|
+
], ": ")
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def render_job_summary(payload)
|
|
148
|
+
content_tag(:span, class: "job-summary") do
|
|
149
|
+
safe_join([
|
|
150
|
+
content_tag(:span, payload["job_class"], class: "job-class"),
|
|
151
|
+
content_tag(:span, "[#{payload["queue_name"]}]", class: "queue-name"),
|
|
152
|
+
payload["duration"] ? content_tag(:span, "#{payload["duration"]}ms", class: "duration") : nil
|
|
153
|
+
].compact, " ")
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Railscope
|
|
4
|
+
class PurgeJob < ApplicationJob
|
|
5
|
+
queue_as :default
|
|
6
|
+
|
|
7
|
+
def perform
|
|
8
|
+
return unless Railscope.enabled?
|
|
9
|
+
|
|
10
|
+
deleted_count = Railscope.storage.destroy_expired!
|
|
11
|
+
Rails.logger.info("[Railscope] Purged #{deleted_count} expired entries")
|
|
12
|
+
deleted_count
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Railscope
|
|
4
|
+
class ApplicationRecord < ActiveRecord::Base
|
|
5
|
+
self.abstract_class = true
|
|
6
|
+
|
|
7
|
+
# Support for separate database connection when RAILSCOPE_DATABASE_URL is configured
|
|
8
|
+
# This isolates Railscope's writes from the main application database,
|
|
9
|
+
# preventing lock contention during high-traffic periods.
|
|
10
|
+
connects_to database: { writing: :railscope, reading: :railscope } if ENV["RAILSCOPE_DATABASE_URL"].present?
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Railscope
|
|
4
|
+
class Entry < ApplicationRecord
|
|
5
|
+
self.table_name = "railscope_entries"
|
|
6
|
+
|
|
7
|
+
validates :entry_type, presence: true
|
|
8
|
+
validates :occurred_at, presence: true
|
|
9
|
+
|
|
10
|
+
# Basic scopes
|
|
11
|
+
scope :by_type, ->(type) { where(entry_type: type) }
|
|
12
|
+
scope :recent, -> { order(occurred_at: :desc) }
|
|
13
|
+
scope :with_tag, ->(tag) { where("? = ANY(tags)", tag) }
|
|
14
|
+
scope :expired, -> { where("occurred_at < ?", Railscope.retention_days.days.ago) }
|
|
15
|
+
|
|
16
|
+
# Telescope-style scopes
|
|
17
|
+
scope :displayable, -> { where(should_display_on_index: true) }
|
|
18
|
+
scope :for_batch, ->(batch_id) { where(batch_id: batch_id) }
|
|
19
|
+
scope :for_family, ->(family_hash) { where(family_hash: family_hash) }
|
|
20
|
+
|
|
21
|
+
# Find by UUID (public identifier)
|
|
22
|
+
def self.find_by_uuid!(uuid)
|
|
23
|
+
find_by!(uuid: uuid)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.find_by_uuid(uuid)
|
|
27
|
+
find_by(uuid: uuid)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Get all related entries in the same batch
|
|
31
|
+
def batch_entries
|
|
32
|
+
return self.class.none if batch_id.blank?
|
|
33
|
+
|
|
34
|
+
self.class.for_batch(batch_id).where.not(id: id).order(:occurred_at)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Get all entries with the same family_hash
|
|
38
|
+
def family_entries
|
|
39
|
+
return self.class.none if family_hash.blank?
|
|
40
|
+
|
|
41
|
+
self.class.for_family(family_hash).where.not(id: id).recent
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Count of similar entries (same family_hash)
|
|
45
|
+
def family_count
|
|
46
|
+
return 0 if family_hash.blank?
|
|
47
|
+
|
|
48
|
+
self.class.for_family(family_hash).count
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" class="dark">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Railscope</title>
|
|
7
|
+
<%= csrf_meta_tags %>
|
|
8
|
+
<link rel="stylesheet" href="<%= railscope.root_path %>assets/app.css">
|
|
9
|
+
</head>
|
|
10
|
+
<body class="bg-[#0d1117] text-[#c9d1d9]">
|
|
11
|
+
<%= yield %>
|
|
12
|
+
<script type="module" src="<%= railscope.root_path %>assets/app.js"></script>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<div id="root"></div>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<div class="railscope-container">
|
|
2
|
+
<header class="railscope-header">
|
|
3
|
+
<h1>Railscope</h1>
|
|
4
|
+
<p class="subtitle"><%= @total_count %> entries</p>
|
|
5
|
+
</header>
|
|
6
|
+
|
|
7
|
+
<nav class="railscope-filters">
|
|
8
|
+
<%= link_to "All", root_path, class: "filter-btn #{@current_type.nil? ? 'active' : ''}" %>
|
|
9
|
+
<% @entry_types.each do |type| %>
|
|
10
|
+
<%= link_to type.titleize, root_path(type: type), class: "filter-btn #{@current_type == type ? 'active' : ''}" %>
|
|
11
|
+
<% end %>
|
|
12
|
+
</nav>
|
|
13
|
+
|
|
14
|
+
<div class="railscope-entries">
|
|
15
|
+
<% if @entries.any? %>
|
|
16
|
+
<table class="entries-table">
|
|
17
|
+
<thead>
|
|
18
|
+
<tr>
|
|
19
|
+
<th>Type</th>
|
|
20
|
+
<th>Summary</th>
|
|
21
|
+
<th>Tags</th>
|
|
22
|
+
<th>Occurred</th>
|
|
23
|
+
</tr>
|
|
24
|
+
</thead>
|
|
25
|
+
<tbody>
|
|
26
|
+
<% @entries.each do |entry| %>
|
|
27
|
+
<tr class="entry-row entry-type-<%= entry.entry_type %>" onclick="window.location='<%= entry_path(entry) %>'" style="cursor: pointer;">
|
|
28
|
+
<td class="entry-type">
|
|
29
|
+
<span class="type-badge <%= entry.entry_type %>"><%= entry.entry_type %></span>
|
|
30
|
+
</td>
|
|
31
|
+
<td class="entry-summary">
|
|
32
|
+
<%= link_to entry_path(entry), class: "entry-link" do %>
|
|
33
|
+
<%= render_entry_summary(entry) %>
|
|
34
|
+
<% end %>
|
|
35
|
+
</td>
|
|
36
|
+
<td class="entry-tags">
|
|
37
|
+
<div class="tags-wrapper">
|
|
38
|
+
<% entry.tags.each do |tag| %>
|
|
39
|
+
<%= link_to tag, root_path(tag: tag, type: @current_type), class: "tag", onclick: "event.stopPropagation();" %>
|
|
40
|
+
<% end %>
|
|
41
|
+
</div>
|
|
42
|
+
</td>
|
|
43
|
+
<td class="entry-time">
|
|
44
|
+
<span title="<%= entry.occurred_at %>"><%= time_ago_in_words(entry.occurred_at) %> ago</span>
|
|
45
|
+
</td>
|
|
46
|
+
</tr>
|
|
47
|
+
<% end %>
|
|
48
|
+
</tbody>
|
|
49
|
+
</table>
|
|
50
|
+
<% else %>
|
|
51
|
+
<div class="empty-state">
|
|
52
|
+
<p>No entries found.</p>
|
|
53
|
+
</div>
|
|
54
|
+
<% end %>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<% if @total_pages > 1 %>
|
|
58
|
+
<nav class="railscope-pagination">
|
|
59
|
+
<% if @current_page > 1 %>
|
|
60
|
+
<%= link_to "Previous", root_path(page: @current_page - 1, type: @current_type, tag: params[:tag]), class: "page-btn" %>
|
|
61
|
+
<% end %>
|
|
62
|
+
|
|
63
|
+
<span class="page-info">Page <%= @current_page %> of <%= @total_pages %></span>
|
|
64
|
+
|
|
65
|
+
<% if @current_page < @total_pages %>
|
|
66
|
+
<%= link_to "Next", root_path(page: @current_page + 1, type: @current_type, tag: params[:tag]), class: "page-btn" %>
|
|
67
|
+
<% end %>
|
|
68
|
+
</nav>
|
|
69
|
+
<% end %>
|
|
70
|
+
</div>
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<div class="railscope-container">
|
|
2
|
+
<header class="railscope-header">
|
|
3
|
+
<nav class="breadcrumb">
|
|
4
|
+
<%= link_to "← Back to entries", root_path, class: "back-link" %>
|
|
5
|
+
</nav>
|
|
6
|
+
<div class="entry-header">
|
|
7
|
+
<span class="type-badge <%= @entry.entry_type %>"><%= @entry.entry_type %></span>
|
|
8
|
+
<h1><%= entry_title(@entry) %></h1>
|
|
9
|
+
</div>
|
|
10
|
+
</header>
|
|
11
|
+
|
|
12
|
+
<div class="entry-detail">
|
|
13
|
+
<div class="detail-grid">
|
|
14
|
+
<div class="detail-main">
|
|
15
|
+
<section class="detail-section">
|
|
16
|
+
<h2>Details</h2>
|
|
17
|
+
<div class="detail-meta">
|
|
18
|
+
<div class="meta-item">
|
|
19
|
+
<span class="meta-label">Occurred at</span>
|
|
20
|
+
<span class="meta-value"><%= @entry.occurred_at.strftime("%Y-%m-%d %H:%M:%S.%L") %></span>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="meta-item">
|
|
23
|
+
<span class="meta-label">Entry ID</span>
|
|
24
|
+
<span class="meta-value mono"><%= @entry.id %></span>
|
|
25
|
+
</div>
|
|
26
|
+
<% if @entry.payload["request_id"] %>
|
|
27
|
+
<div class="meta-item">
|
|
28
|
+
<span class="meta-label">Request ID</span>
|
|
29
|
+
<span class="meta-value mono"><%= @entry.payload["request_id"] %></span>
|
|
30
|
+
</div>
|
|
31
|
+
<% end %>
|
|
32
|
+
<% if @entry.payload["duration"] %>
|
|
33
|
+
<div class="meta-item">
|
|
34
|
+
<span class="meta-label">Duration</span>
|
|
35
|
+
<span class="meta-value"><%= @entry.payload["duration"] %>ms</span>
|
|
36
|
+
</div>
|
|
37
|
+
<% end %>
|
|
38
|
+
</div>
|
|
39
|
+
</section>
|
|
40
|
+
|
|
41
|
+
<section class="detail-section">
|
|
42
|
+
<h2>Tags</h2>
|
|
43
|
+
<div class="tags-wrapper">
|
|
44
|
+
<% @entry.tags.each do |tag| %>
|
|
45
|
+
<%= link_to tag, root_path(tag: tag), class: "tag" %>
|
|
46
|
+
<% end %>
|
|
47
|
+
</div>
|
|
48
|
+
</section>
|
|
49
|
+
|
|
50
|
+
<section class="detail-section">
|
|
51
|
+
<h2>Payload</h2>
|
|
52
|
+
<div class="json-viewer">
|
|
53
|
+
<%= render_json(@entry.payload) %>
|
|
54
|
+
</div>
|
|
55
|
+
</section>
|
|
56
|
+
|
|
57
|
+
<% if @entry.entry_type == "exception" && @entry.payload["backtrace"].present? %>
|
|
58
|
+
<section class="detail-section">
|
|
59
|
+
<h2>Backtrace</h2>
|
|
60
|
+
<div class="backtrace">
|
|
61
|
+
<% @entry.payload["backtrace"].each_with_index do |line, i| %>
|
|
62
|
+
<div class="backtrace-line">
|
|
63
|
+
<span class="line-number"><%= i + 1 %></span>
|
|
64
|
+
<code><%= line %></code>
|
|
65
|
+
</div>
|
|
66
|
+
<% end %>
|
|
67
|
+
</div>
|
|
68
|
+
</section>
|
|
69
|
+
<% end %>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<div class="detail-sidebar">
|
|
73
|
+
<% if @related_entries.any? %>
|
|
74
|
+
<section class="detail-section">
|
|
75
|
+
<h2>Timeline</h2>
|
|
76
|
+
<p class="timeline-info">Events from the same request</p>
|
|
77
|
+
<div class="timeline">
|
|
78
|
+
<% @related_entries.each do |entry| %>
|
|
79
|
+
<div class="timeline-item <%= entry.id == @entry.id ? 'current' : '' %>">
|
|
80
|
+
<%= link_to entry_path(entry), class: "timeline-link" do %>
|
|
81
|
+
<span class="timeline-type type-badge <%= entry.entry_type %>"><%= entry.entry_type %></span>
|
|
82
|
+
<span class="timeline-summary"><%= timeline_summary(entry) %></span>
|
|
83
|
+
<span class="timeline-time"><%= entry.payload["duration"] ? "#{entry.payload['duration']}ms" : "" %></span>
|
|
84
|
+
<% end %>
|
|
85
|
+
</div>
|
|
86
|
+
<% end %>
|
|
87
|
+
</div>
|
|
88
|
+
</section>
|
|
89
|
+
<% end %>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
data/client/.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/node_modules
|
data/client/index.html
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" class="dark">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Railscope</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body class="bg-dark-bg text-dark-text">
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|