flare 0.1.1 → 1.0.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.

Potentially problematic release.


This version of flare might be problematic. Click here for more details.

Files changed (56) hide show
  1. data/.document +5 -0
  2. data/.gitignore +22 -0
  3. data/LICENSE +20 -0
  4. data/README.rdoc +18 -0
  5. data/Rakefile +55 -0
  6. data/VERSION +1 -0
  7. data/flare.gemspec +66 -0
  8. data/lib/flare/active_record.rb +102 -0
  9. data/lib/flare/collection.rb +47 -0
  10. data/lib/flare/configuration.rb +64 -109
  11. data/lib/flare/index_builder.rb +24 -0
  12. data/lib/flare/session.rb +142 -0
  13. data/lib/flare/tasks.rb +18 -0
  14. data/lib/flare.rb +19 -408
  15. data/test/helper.rb +10 -0
  16. data/test/test_flare.rb +7 -0
  17. metadata +89 -228
  18. checksums.yaml +0 -7
  19. data/CHANGELOG.md +0 -5
  20. data/LICENSE.txt +0 -21
  21. data/README.md +0 -148
  22. data/app/controllers/flare/application_controller.rb +0 -22
  23. data/app/controllers/flare/jobs_controller.rb +0 -55
  24. data/app/controllers/flare/requests_controller.rb +0 -73
  25. data/app/controllers/flare/spans_controller.rb +0 -101
  26. data/app/helpers/flare/application_helper.rb +0 -168
  27. data/app/views/flare/jobs/index.html.erb +0 -69
  28. data/app/views/flare/jobs/show.html.erb +0 -323
  29. data/app/views/flare/requests/index.html.erb +0 -120
  30. data/app/views/flare/requests/show.html.erb +0 -498
  31. data/app/views/flare/spans/index.html.erb +0 -112
  32. data/app/views/flare/spans/show.html.erb +0 -184
  33. data/app/views/layouts/flare/application.html.erb +0 -126
  34. data/config/routes.rb +0 -20
  35. data/exe/flare +0 -9
  36. data/lib/flare/backoff_policy.rb +0 -73
  37. data/lib/flare/cli/doctor_command.rb +0 -129
  38. data/lib/flare/cli/output.rb +0 -45
  39. data/lib/flare/cli/setup_command.rb +0 -404
  40. data/lib/flare/cli/status_command.rb +0 -47
  41. data/lib/flare/cli.rb +0 -50
  42. data/lib/flare/engine.rb +0 -43
  43. data/lib/flare/http_metrics_config.rb +0 -101
  44. data/lib/flare/metric_counter.rb +0 -45
  45. data/lib/flare/metric_flusher.rb +0 -124
  46. data/lib/flare/metric_key.rb +0 -42
  47. data/lib/flare/metric_span_processor.rb +0 -470
  48. data/lib/flare/metric_storage.rb +0 -42
  49. data/lib/flare/metric_submitter.rb +0 -221
  50. data/lib/flare/source_location.rb +0 -113
  51. data/lib/flare/sqlite_exporter.rb +0 -279
  52. data/lib/flare/storage/sqlite.rb +0 -789
  53. data/lib/flare/storage.rb +0 -54
  54. data/lib/flare/version.rb +0 -5
  55. data/public/flare-assets/flare.css +0 -1245
  56. data/public/flare-assets/images/flipper.png +0 -0
@@ -1,101 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Flare
4
- class SpansController < ApplicationController
5
- around_action :untrace_request
6
-
7
- helper_method :current_section, :page_title, :category_config
8
-
9
- PER_PAGE = 50
10
-
11
- CATEGORIES = {
12
- "queries" => { title: "Queries", icon: "database", badge_class: "sql" },
13
- "cache" => { title: "Cache", icon: "archive", badge_class: "cache" },
14
- "views" => { title: "Views", icon: "layout", badge_class: "view" },
15
- "http" => { title: "HTTP", icon: "globe", badge_class: "http" },
16
- "mail" => { title: "Mail", icon: "mail", badge_class: "mail" },
17
- "redis" => { title: "Redis", icon: "database", badge_class: "other" },
18
- "exceptions" => { title: "Exceptions", icon: "alert-triangle", badge_class: "exception" }
19
- }.freeze
20
-
21
- def queries
22
- list_spans("queries")
23
- end
24
-
25
- def cache
26
- list_spans("cache")
27
- end
28
-
29
- def views
30
- list_spans("views")
31
- end
32
-
33
- def http
34
- list_spans("http")
35
- end
36
-
37
- def mail
38
- list_spans("mail")
39
- end
40
-
41
- def redis
42
- list_spans("redis")
43
- end
44
-
45
- def exceptions
46
- list_spans("exceptions")
47
- end
48
-
49
- def show
50
- @span = Flare.storage.find_span(params[:id])
51
-
52
- if @span.blank?
53
- redirect_to requests_path, alert: "Span not found"
54
- return
55
- end
56
-
57
- @category = params[:category]
58
- end
59
-
60
- private
61
-
62
- def list_spans(category)
63
- @category = category
64
- @offset = params[:offset].to_i
65
- filter_params = { name: params[:name].presence }
66
-
67
- spans = Flare.storage.list_spans_by_category(category, **filter_params, limit: PER_PAGE + 1, offset: @offset)
68
- @total_count = Flare.storage.count_spans_by_category(category, **filter_params)
69
- @has_next = spans.size > PER_PAGE
70
- @spans = spans.first(PER_PAGE)
71
- @has_prev = @offset > 0
72
-
73
- # Load properties for display
74
- span_ids = @spans.map { |s| s[:id] }
75
- if span_ids.any?
76
- all_properties = Flare.storage.load_properties_for_ids("Flare::Span", span_ids)
77
- @spans.each do |span|
78
- span[:properties] = all_properties[span[:id]] || {}
79
- end
80
- end
81
-
82
- render :index
83
- end
84
-
85
- def untrace_request
86
- Flare.untraced { yield }
87
- end
88
-
89
- def current_section
90
- @category || "spans"
91
- end
92
-
93
- def page_title
94
- category_config[:title]
95
- end
96
-
97
- def category_config
98
- CATEGORIES[@category] || { title: "Spans", icon: "activity", badge_class: "other" }
99
- end
100
- end
101
- end
@@ -1,168 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Flare
4
- module ApplicationHelper
5
- def span_category(span)
6
- name = span[:name].to_s.downcase
7
- case name
8
- when /sql\.active_record/, /mysql/, /postgres/, /sqlite/
9
- "sql"
10
- when /cache/
11
- "cache"
12
- when /render/, /view/
13
- "view"
14
- when /http/, /net_http/, /faraday/
15
- "http"
16
- when /mail/
17
- "mailer"
18
- when /job/, /active_job/
19
- "job"
20
- when /action_controller/, /process_action/
21
- "controller"
22
- else
23
- "other"
24
- end
25
- end
26
-
27
- def span_display_info(span, category)
28
- props = span[:properties] || {}
29
- case category
30
- when "queries"
31
- stmt = props["db.statement"]&.to_s
32
- name = props["name"]&.to_s
33
- db_name = props["db.name"]&.to_s
34
- source_loc = if props["code.filepath"] && props["code.lineno"]
35
- "#{props["code.filepath"]}:#{props["code.lineno"]}"
36
- end
37
- secondary = [name.presence, db_name.presence, source_loc].compact.join(" \u00b7 ").presence
38
- if stmt.present?
39
- { primary: stmt, secondary: secondary }
40
- elsif name.present?
41
- { primary: name, secondary: [db_name.presence, source_loc].compact.join(" \u00b7 ").presence }
42
- else
43
- { primary: span[:name], secondary: [db_name.presence, source_loc].compact.join(" \u00b7 ").presence }
44
- end
45
- when "cache"
46
- key = props["key"]&.to_s
47
- op = span[:name].to_s.sub(".active_support", "").sub("cache_", "")
48
- store = props["store"]&.to_s&.sub(/^ActiveSupport::Cache::/, "")
49
- { primary: key.presence || span[:name], secondary: store, cache_op: op }
50
- when "views"
51
- identifier = props["identifier"] || props["code.filepath"]
52
- primary = identifier ? identifier.to_s.sub(/^.*\/app\/views\//, "") : span[:name]
53
- { primary: primary, secondary: nil }
54
- when "http"
55
- full_url = props["http.url"] || ""
56
- target = props["http.target"] || ""
57
- host = props["http.host"] || props["net.peer.name"] || props["peer.service"]
58
- uri = URI.parse(full_url) rescue nil
59
- if uri && uri.host
60
- domain = uri.host
61
- path = uri.path.presence || "/"
62
- path = "#{path}?#{uri.query}" if uri.query.present?
63
- else
64
- domain = host
65
- path = target.presence || full_url
66
- end
67
- method = props["http.method"]
68
- status = props["http.status_code"]
69
- { primary: path.to_s.truncate(100), secondary: domain, http_method: method, http_status: status }
70
- when "mail"
71
- mailer = props["mailer"]
72
- action = props["action"]
73
- subject = props["subject"]
74
- if mailer && action
75
- { primary: "#{mailer}##{action}", secondary: subject }
76
- else
77
- { primary: span[:name], secondary: nil }
78
- end
79
- when "redis"
80
- cmd = props["db.statement"]&.to_s
81
- { primary: cmd.presence || span[:name], secondary: nil }
82
- when "exceptions"
83
- exc_type = span[:exception_type]
84
- exc_message = span[:exception_message]
85
- primary = if exc_type.present? && exc_message.present?
86
- "#{exc_type}: #{exc_message}"
87
- elsif exc_message.present?
88
- exc_message
89
- elsif exc_type.present?
90
- exc_type
91
- else
92
- span[:name]
93
- end
94
- stacktrace = span[:exception_stacktrace].to_s
95
- first_app_line = stacktrace.split("\n").find { |line| line.include?("/app/") } || stacktrace.split("\n").first
96
- secondary = first_app_line&.strip.to_s.truncate(200)
97
- { primary: primary, secondary: secondary }
98
- else
99
- { primary: span[:name], secondary: nil }
100
- end
101
- end
102
-
103
- def format_duration(ms)
104
- return "-" if ms.nil?
105
-
106
- if ms >= 1000
107
- "#{(ms / 1000.0).round(1)}s"
108
- else
109
- "#{ms.round(1)}ms"
110
- end
111
- end
112
-
113
- def format_content(data, indent = 0)
114
- return "" if data.nil?
115
-
116
- lines = []
117
- prefix = " " * indent
118
-
119
- case data
120
- when Hash
121
- data.each do |key, value|
122
- if value.is_a?(Hash) || value.is_a?(Array)
123
- lines << "#{prefix}#{key}:"
124
- lines << format_content(value, indent + 1)
125
- else
126
- formatted_value = format_value(value)
127
- if formatted_value.include?("\n")
128
- lines << "#{prefix}#{key}:"
129
- formatted_value.each_line do |line|
130
- lines << "#{prefix} #{line.rstrip}"
131
- end
132
- else
133
- lines << "#{prefix}#{key}: #{formatted_value}"
134
- end
135
- end
136
- end
137
- when Array
138
- data.each do |item|
139
- if item.is_a?(Hash) || item.is_a?(Array)
140
- lines << "#{prefix}-"
141
- lines << format_content(item, indent + 1)
142
- else
143
- lines << "#{prefix}- #{format_value(item)}"
144
- end
145
- end
146
- else
147
- lines << "#{prefix}#{format_value(data)}"
148
- end
149
-
150
- lines.join("\n")
151
- end
152
-
153
- private
154
-
155
- def format_value(value)
156
- case value
157
- when nil
158
- "null"
159
- when true, false
160
- value.to_s
161
- when Numeric
162
- value.to_s
163
- else
164
- value.to_s
165
- end
166
- end
167
- end
168
- end
@@ -1,69 +0,0 @@
1
- <div class="page-header">
2
- <h1 class="page-title"><%= page_title %></h1>
3
- <div class="filters">
4
- <%= form_tag(jobs_path, method: :get, id: "filter-form") do %>
5
- <%= text_field_tag :name, params[:name], placeholder: "Search...", class: "filter-select", style: "min-width: 200px;", onchange: "this.form.submit()" %>
6
- <% end %>
7
- </div>
8
- </div>
9
-
10
- <div class="page-body">
11
- <div class="card">
12
- <div class="card-body">
13
- <% if @jobs.blank? %>
14
- <div class="empty-state">
15
- <div class="empty-state-icon">
16
- <i class="bi bi-briefcase" style="font-size: 1.5rem;"></i>
17
- </div>
18
- <h3>No jobs found</h3>
19
- <p>Data will appear here as your application processes background jobs.</p>
20
- </div>
21
- <% else %>
22
- <table class="data-table">
23
- <thead>
24
- <tr>
25
- <th>Job</th>
26
- <th style="width: 100px; text-align: right;">Duration</th>
27
- <th style="width: 120px; text-align: right;">Happened</th>
28
- </tr>
29
- </thead>
30
- <tbody>
31
- <% @jobs.each do |job| %>
32
- <% started_at = Time.at(job[:start_timestamp] / 1_000_000_000.0) rescue nil %>
33
- <%
34
- display_name = job[:job_class] || job[:name].to_s.sub(/ (process|publish)$/, '')
35
- %>
36
- <tr onclick="navigateIfNotSelecting('<%= job_path(job[:trace_id]) %>')" style="cursor: pointer;">
37
- <td class="path-cell">
38
- <span class="job-name path-text" title="<%= display_name %>"><%= display_name %></span>
39
- <% queue = job[:queue_name] || "default" %>
40
- <span class="path-subtext" title="<%= queue %>"><%= queue %></span>
41
- </td>
42
- <td class="duration" style="text-align: right;"><%= format_duration(job[:duration_ms]) %></td>
43
- <td class="timestamp" style="text-align: right;"><%= started_at ? time_ago_in_words(started_at) : "-" %></td>
44
- </tr>
45
- <% end %>
46
- </tbody>
47
- </table>
48
-
49
- <% if @total_count > 0 %>
50
- <div class="pagination">
51
- <div class="pagination-side">
52
- <% if @has_prev %>
53
- <%= link_to "Previous", jobs_path(request.query_parameters.merge(offset: [@offset - 50, 0].max)), class: "pagination-link" %>
54
- <% end %>
55
- </div>
56
- <div class="pagination-info">
57
- <%= number_with_delimiter(@offset + 1) %> - <%= number_with_delimiter([@offset + @jobs.size, @total_count].min) %> of <%= number_with_delimiter(@total_count) %>
58
- </div>
59
- <div class="pagination-side" style="text-align: right;">
60
- <% if @has_next %>
61
- <%= link_to "Next", jobs_path(request.query_parameters.merge(offset: @offset + 50)), class: "pagination-link" %>
62
- <% end %>
63
- </div>
64
- </div>
65
- <% end %>
66
- <% end %>
67
- </div>
68
- </div>
69
- </div>
@@ -1,323 +0,0 @@
1
- <%
2
- # Get root span properties
3
- root_props = @root_span ? @root_span[:properties] : {}
4
- job_class = root_props["code.namespace"]
5
- queue_name = root_props["messaging.destination"]
6
- job_id = root_props["messaging.message.id"] || root_props["messaging.message_id"]
7
-
8
- # Calculate total duration from root span
9
- total_duration_ms = @root_span ? @root_span[:duration_ms] : 1
10
- root_start = @root_span ? @root_span[:start_timestamp] : 0
11
- started_at = @root_span ? Time.at(@root_span[:start_timestamp] / 1_000_000_000.0) : nil
12
-
13
- # Extract job name from span name
14
- job_display_name = job_class || @job[:name].to_s.sub(/ (process|publish)$/, '')
15
-
16
- # Count span categories
17
- span_counts = @child_spans.each_with_object(Hash.new(0)) do |span, counts|
18
- counts[span_category(span)] += 1
19
- end
20
- %>
21
-
22
- <div class="page-header">
23
- <div class="detail-header">
24
- <div class="detail-header-left">
25
- <div class="detail-title">
26
- <span class="badge badge-job">Job</span>
27
- <h1><%= job_display_name %></h1>
28
- </div>
29
- <div class="detail-meta">
30
- <div class="meta-item">
31
- <span class="meta-label">Duration</span>
32
- <span class="meta-value mono"><%= format_duration(total_duration_ms) %></span>
33
- </div>
34
- <div class="meta-item">
35
- <span class="meta-label">Time</span>
36
- <span class="meta-value"><%= started_at&.strftime("%b %d, %Y %H:%M:%S") %></span>
37
- </div>
38
- <% if queue_name %>
39
- <div class="meta-item">
40
- <span class="meta-label">Queue</span>
41
- <span class="meta-value"><%= queue_name %></span>
42
- </div>
43
- <% end %>
44
- <% if job_id %>
45
- <div class="meta-item">
46
- <span class="meta-label">Job ID</span>
47
- <span class="meta-value mono"><%= job_id %></span>
48
- </div>
49
- <% end %>
50
- </div>
51
- </div>
52
- </div>
53
- </div>
54
-
55
- <div class="page-body">
56
- <div class="card">
57
- <div class="tabs">
58
- <a class="tab active" onclick="showTab('timeline')">Timeline (<%= @child_spans.size %>)</a>
59
- <a class="tab" onclick="showTab('details')">Details</a>
60
- </div>
61
-
62
- <div id="tab-details" class="tab-content">
63
- <%
64
- shown_keys = %w[code.namespace messaging.destination messaging.message.id messaging.message_id]
65
- other_props = root_props.reject { |k, v| shown_keys.include?(k) || v.is_a?(Hash) || v.is_a?(Array) }
66
- %>
67
- <div class="details-sections">
68
- <!-- Job Section -->
69
- <div class="details-section">
70
- <div class="section-header">
71
- <div class="section-icon job">
72
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
73
- <rect x="2" y="7" width="20" height="14" rx="2" ry="2"></rect>
74
- <path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"></path>
75
- </svg>
76
- </div>
77
- <span class="section-title">Job</span>
78
- </div>
79
- <div class="section-grid">
80
- <div class="detail-item">
81
- <span class="detail-label">Duration</span>
82
- <span class="detail-value prominent"><%= format_duration(total_duration_ms) %></span>
83
- </div>
84
- <div class="detail-item">
85
- <span class="detail-label">Time</span>
86
- <span class="detail-value"><%= started_at&.strftime("%b %d, %Y at %H:%M:%S") %></span>
87
- </div>
88
- <% if job_class %>
89
- <div class="detail-item">
90
- <span class="detail-label">Job Class</span>
91
- <span class="detail-value mono"><%= job_class %></span>
92
- </div>
93
- <% end %>
94
- <% if job_id %>
95
- <div class="detail-item full-width">
96
- <span class="detail-label">Job ID</span>
97
- <span class="detail-value mono secondary"><%= job_id %></span>
98
- </div>
99
- <% end %>
100
- </div>
101
- </div>
102
-
103
- <!-- Queue Section -->
104
- <% if queue_name %>
105
- <div class="details-section">
106
- <div class="section-header">
107
- <div class="section-icon queue">
108
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
109
- <line x1="8" y1="6" x2="21" y2="6"></line>
110
- <line x1="8" y1="12" x2="21" y2="12"></line>
111
- <line x1="8" y1="18" x2="21" y2="18"></line>
112
- <line x1="3" y1="6" x2="3.01" y2="6"></line>
113
- <line x1="3" y1="12" x2="3.01" y2="12"></line>
114
- <line x1="3" y1="18" x2="3.01" y2="18"></line>
115
- </svg>
116
- </div>
117
- <span class="section-title">Queue</span>
118
- </div>
119
- <div class="section-grid">
120
- <div class="detail-item">
121
- <span class="detail-label">Queue Name</span>
122
- <span class="detail-value mono"><%= queue_name %></span>
123
- </div>
124
- </div>
125
- </div>
126
- <% end %>
127
-
128
- <!-- Other Properties Section -->
129
- <% if other_props.any? %>
130
- <div class="details-section">
131
- <div class="section-header">
132
- <div class="section-icon other">
133
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
134
- <circle cx="12" cy="12" r="1"></circle>
135
- <circle cx="19" cy="12" r="1"></circle>
136
- <circle cx="5" cy="12" r="1"></circle>
137
- </svg>
138
- </div>
139
- <span class="section-title">Additional Properties</span>
140
- </div>
141
- <div class="section-grid">
142
- <% other_props.each do |key, value| %>
143
- <div class="detail-item">
144
- <span class="detail-label"><%= key.to_s.split(".").last.titleize %></span>
145
- <span class="detail-value mono truncate" title="<%= value %>"><%= value %></span>
146
- </div>
147
- <% end %>
148
- </div>
149
- </div>
150
- <% end %>
151
-
152
- <% if root_props.blank? %>
153
- <div class="details-section">
154
- <div class="empty-clues" style="padding: 1.25rem;">
155
- <p>No details recorded.</p>
156
- </div>
157
- </div>
158
- <% end %>
159
- </div>
160
- </div>
161
-
162
- <div id="tab-timeline" class="tab-content active">
163
- <% if @child_spans.blank? %>
164
- <div class="empty-clues">
165
- <i class="bi bi-clock-history" style="font-size: 2rem; color: var(--cb-warm-gray-light);"></i>
166
- <p>No timeline events recorded for this job.</p>
167
- </div>
168
- <% else %>
169
- <div class="legend">
170
- <div class="legend-item clickable active" data-filter="all" onclick="filterSpans('all')"><div class="legend-color all"></div> All (<%= @child_spans.size %>)</div>
171
- <% if span_counts["sql"] > 0 %><div class="legend-item clickable" data-filter="sql" onclick="filterSpans('sql')"><div class="legend-color sql"></div> SQL (<%= span_counts["sql"] %>)</div><% end %>
172
- <% if span_counts["cache"] > 0 %><div class="legend-item clickable" data-filter="cache" onclick="filterSpans('cache')"><div class="legend-color cache"></div> Cache (<%= span_counts["cache"] %>)</div><% end %>
173
- <% if span_counts["view"] > 0 %><div class="legend-item clickable" data-filter="view" onclick="filterSpans('view')"><div class="legend-color view"></div> View (<%= span_counts["view"] %>)</div><% end %>
174
- <% if span_counts["http"] > 0 %><div class="legend-item clickable" data-filter="http" onclick="filterSpans('http')"><div class="legend-color http"></div> HTTP (<%= span_counts["http"] %>)</div><% end %>
175
- <% if span_counts["mailer"] > 0 %><div class="legend-item clickable" data-filter="mailer" onclick="filterSpans('mailer')"><div class="legend-color mailer"></div> Mailer (<%= span_counts["mailer"] %>)</div><% end %>
176
- <% if span_counts["job"] > 0 %><div class="legend-item clickable" data-filter="job" onclick="filterSpans('job')"><div class="legend-color job"></div> Job (<%= span_counts["job"] %>)</div><% end %>
177
- <% if span_counts["controller"] > 0 %><div class="legend-item clickable" data-filter="controller" onclick="filterSpans('controller')"><div class="legend-color controller"></div> Controller (<%= span_counts["controller"] %>)</div><% end %>
178
- <% if span_counts["other"] > 0 %><div class="legend-item clickable" data-filter="other" onclick="filterSpans('other')"><div class="legend-color other"></div> Other (<%= span_counts["other"] %>)</div><% end %>
179
- <div style="margin-left: auto;">
180
- <button type="button" onclick="collapseAll()" class="collapse-btn">
181
- <i class="bi bi-arrows-collapse"></i>
182
- Collapse All
183
- </button>
184
- </div>
185
- </div>
186
-
187
- <div style="padding: 0.75rem;">
188
- <div class="waterfall-header">
189
- <div>Event</div>
190
- <div>Duration</div>
191
- </div>
192
-
193
- <div class="waterfall">
194
- <% @child_spans.each_with_index do |span, index| %>
195
- <%
196
- offset_ns = span[:start_timestamp] - root_start
197
- offset_ms = offset_ns / 1_000_000.0
198
- left = (offset_ms / [total_duration_ms, 1].max * 100).clamp(0, 99.5)
199
- width = (span[:duration_ms] / [total_duration_ms, 1].max * 100).clamp(0.5, [0.5, 100 - left].max)
200
- category = span_category(span)
201
-
202
- props = span[:properties] || {}
203
- sql_statement = nil
204
- display_name = if span[:name] == "instantiation.active_record"
205
- class_name = props["class_name"] || "Record"
206
- count = props["record_count"]
207
- count ? "#{class_name} Instantiation (#{count})" : "#{class_name} Instantiation"
208
- elsif category == "sql"
209
- sql_statement = props["db.statement"]
210
- props["name"] || span[:name]
211
- elsif category == "cache"
212
- key = props["key"]
213
- op = span[:name].to_s.sub(".active_support", "").sub("cache_", "")
214
- key ? "#{op}: #{key.to_s.truncate(80)}" : span[:name]
215
- elsif category == "view"
216
- identifier = props["identifier"] || props["code.filepath"]
217
- if identifier
218
- identifier.to_s.sub(/^.*\/app\/views\//, "")
219
- else
220
- span[:name]
221
- end
222
- elsif category == "http"
223
- url = props["http.url"] || props["http.target"]
224
- method = props["http.method"]
225
- "#{method} #{url}".strip.presence || span[:name]
226
- elsif category == "controller"
227
- ns = props["code.namespace"]
228
- fn = props["code.function"]
229
- ns && fn ? "#{ns}##{fn}" : span[:name]
230
- else
231
- span[:name]
232
- end
233
- %>
234
- <div class="waterfall-row" onclick="toggleSpan(<%= index %>)" id="row-<%= index %>" data-span-type="<%= category %>" <% if sql_statement.present? %>data-sql="<%= sql_statement.truncate(500).gsub('"', "'") %>"<% end %>>
235
- <div class="waterfall-bar-container">
236
- <div class="waterfall-bar <%= category %>" style="left: <%= left %>%; width: <%= width %>%;"></div>
237
- <span class="waterfall-label"><%= display_name %></span>
238
- <span class="waterfall-duration"><%= format_duration(span[:duration_ms]) %></span>
239
- </div>
240
- </div>
241
- <div class="span-detail <%= category %>" id="span-<%= index %>">
242
- <pre><code><%= format_content(span[:properties]) %></code></pre>
243
- </div>
244
- <% end %>
245
- </div>
246
- </div>
247
- <% end %>
248
- </div>
249
- </div>
250
- </div>
251
-
252
- <script>
253
- function showTab(tabName) {
254
- document.querySelectorAll('.tab-content').forEach(function(el) { el.classList.remove('active'); });
255
- document.querySelectorAll('.tab').forEach(function(el) { el.classList.remove('active'); });
256
- document.getElementById('tab-' + tabName).classList.add('active');
257
- event.target.classList.add('active');
258
- }
259
-
260
- function toggleSpan(index) {
261
- const detail = document.getElementById('span-' + index);
262
- const row = document.getElementById('row-' + index);
263
- if (detail.classList.contains('visible')) {
264
- detail.classList.remove('visible');
265
- row.classList.remove('selected');
266
- } else {
267
- detail.classList.add('visible');
268
- row.classList.add('selected');
269
- }
270
- }
271
-
272
- function collapseAll() {
273
- document.querySelectorAll('.span-detail.visible').forEach(function(el) { el.classList.remove('visible'); });
274
- document.querySelectorAll('.waterfall-row.selected').forEach(function(el) { el.classList.remove('selected'); });
275
- }
276
-
277
- function filterSpans(filterType) {
278
- document.querySelectorAll('.legend-item.clickable').forEach(function(el) { el.classList.remove('active'); });
279
- document.querySelector('.legend-item[data-filter="' + filterType + '"]').classList.add('active');
280
- document.querySelectorAll('.waterfall-row').forEach(function(row) {
281
- const spanType = row.getAttribute('data-span-type');
282
- if (filterType === 'all' || spanType === filterType) {
283
- row.classList.remove('hidden');
284
- } else {
285
- row.classList.add('hidden');
286
- row.classList.remove('selected');
287
- }
288
- });
289
- document.querySelectorAll('.span-detail').forEach(function(detail) {
290
- const index = detail.id.replace('span-', '');
291
- const row = document.getElementById('row-' + index);
292
- if (row.classList.contains('hidden')) {
293
- detail.classList.add('hidden-by-filter');
294
- detail.classList.remove('visible');
295
- } else {
296
- detail.classList.remove('hidden-by-filter');
297
- }
298
- });
299
- }
300
-
301
- // SQL tooltip
302
- document.addEventListener('DOMContentLoaded', function() {
303
- const sqlTooltip = document.getElementById('sql-tooltip');
304
- let tooltipTimeout;
305
- document.querySelectorAll('.waterfall-row[data-sql]').forEach(function(row) {
306
- row.addEventListener('mouseenter', function() {
307
- const sql = row.getAttribute('data-sql');
308
- if (sql && !row.classList.contains('selected')) {
309
- tooltipTimeout = setTimeout(function() {
310
- sqlTooltip.textContent = sql;
311
- sqlTooltip.classList.add('visible');
312
- }, 200);
313
- }
314
- });
315
- row.addEventListener('mouseleave', function() {
316
- clearTimeout(tooltipTimeout);
317
- sqlTooltip.classList.remove('visible');
318
- });
319
- });
320
- });
321
- </script>
322
-
323
- <div id="sql-tooltip"></div>