activeinsights 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/README.md +72 -0
- data/Rakefile +24 -0
- data/app/assets/javascripts/active_insights/application.js +5 -0
- data/app/controllers/active_insights/application_controller.rb +25 -0
- data/app/controllers/active_insights/controller_p_values_controller.rb +25 -0
- data/app/controllers/active_insights/p_values_controller.rb +23 -0
- data/app/controllers/active_insights/requests_controller.rb +13 -0
- data/app/controllers/active_insights/rpm_controller.rb +16 -0
- data/app/helpers/active_insights/application_helper.rb +47 -0
- data/app/models/active_insights/record.rb +9 -0
- data/app/models/active_insights/request.rb +71 -0
- data/app/views/active_insights/controller_p_values/index.html.erb +26 -0
- data/app/views/active_insights/p_values/index.html.erb +26 -0
- data/app/views/active_insights/requests/index.html.erb +54 -0
- data/app/views/active_insights/rpm/index.html.erb +26 -0
- data/app/views/layouts/active_insights/application.html.erb +345 -0
- data/config/initializers/importmap.rb +15 -0
- data/config/routes.rb +19 -0
- data/db/migrate/20240111225806_active_insights_request.rb +32 -0
- data/lib/active_insights/engine.rb +39 -0
- data/lib/active_insights/seeder.rb +66 -0
- data/lib/active_insights/version.rb +5 -0
- data/lib/active_insights.rb +11 -0
- data/lib/activeinsights.rb +3 -0
- data/lib/generators/active_insights/install/install_generator.rb +11 -0
- metadata +111 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: e4eaa4679af5bdd26c11fee43867a9fb1dd49604962280c7adf7542a55e996ff
|
4
|
+
data.tar.gz: 8cf132a8cb971a91c0f3bac654de0cfc43e6ee6a0d3e858a5ef09bffcf3fdd01
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 70cad20e56ce23648fae3e31e35846fdaa936e90326e81767ab80a65363b3e5a63ca4d39db2496584ba9318c7e6f836e368f2551baf8dec5bbb28194e668c3fc
|
7
|
+
data.tar.gz: 940ee1ff0796fea342a072aadd51e1f81224e6c1f26cbd4f392fa5552630cb78b07c82bad331d729f3b2dd5e02e3f77f206bf674f0684dc2a2b576012b8a6b10
|
data/README.md
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
# ActiveInsights
|
2
|
+
|
3
|
+
One of the fundemental tools needed to take your Rails app to production is a
|
4
|
+
way to track response times. Unfortunately, theres no free, easy,
|
5
|
+
open source way to track them for small or medium apps. Skylight, Honeybadger,
|
6
|
+
Sentry, and AppSignal are great, but they are are closed source and
|
7
|
+
there should be an easy open source alternative where you control the data.
|
8
|
+
|
9
|
+
ActiveInsights hooks into the ActiveSupport [instrumention](https://guides.rubyonrails.org/active_support_instrumentation.html#) baked directly into Rails. ActiveInsights tracks RPM, RPM per controller, and p50/p95/p99 response times and charts all those by the minute.
|
10
|
+
|
11
|
+

|
12
|
+

|
13
|
+

|
14
|
+

|
15
|
+
|
16
|
+
## Installation
|
17
|
+
Add this line to your application's Gemfile:
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
gem "activeinsights"
|
21
|
+
```
|
22
|
+
|
23
|
+
And then execute:
|
24
|
+
```bash
|
25
|
+
$ bundle
|
26
|
+
```
|
27
|
+
|
28
|
+
Or install it yourself as:
|
29
|
+
```bash
|
30
|
+
$ gem install activeinsights
|
31
|
+
```
|
32
|
+
|
33
|
+
And then install migrations:
|
34
|
+
```bash
|
35
|
+
bin/rails active_insights:install
|
36
|
+
bin/rails rails db:migrate
|
37
|
+
```
|
38
|
+
|
39
|
+
This also mounts a route in your routes file to view the insights at `/insights`.
|
40
|
+
|
41
|
+
|
42
|
+
##### Config
|
43
|
+
|
44
|
+
You can supply a hash of connection options to `connects_to` set the connection
|
45
|
+
options for the `Request` model.
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
ActiveInsights.connects_to = { database: { writing: :requests, reading: :requests } }
|
49
|
+
```
|
50
|
+
|
51
|
+
## Development
|
52
|
+
|
53
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
54
|
+
`rails test` to run the unit tests.
|
55
|
+
|
56
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To
|
57
|
+
release a new version, execute `bin/publish (major|minor|patch)` which will
|
58
|
+
update the version number in `version.rb`, create a git tag for the version,
|
59
|
+
push git commits and tags, and push the `.gem` file to GitHub.
|
60
|
+
|
61
|
+
## Contributing
|
62
|
+
|
63
|
+
Bug reports and pull requests are welcome on
|
64
|
+
[GitHub](https://github.com/npezza93/activeinsights). This project is intended to
|
65
|
+
be a safe, welcoming space for collaboration, and contributors are expected to
|
66
|
+
adhere to the [Contributor Covenant](http://contributor-covenant.org) code of
|
67
|
+
conduct.
|
68
|
+
|
69
|
+
## License
|
70
|
+
|
71
|
+
The gem is available as open source under the terms of the
|
72
|
+
[MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
begin
|
4
|
+
require "bundler/setup"
|
5
|
+
rescue LoadError
|
6
|
+
puts "You must `gem install bundler` and `bundle install` to run rake tasks"
|
7
|
+
end
|
8
|
+
|
9
|
+
APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
|
10
|
+
load "rails/tasks/engine.rake"
|
11
|
+
|
12
|
+
load "rails/tasks/statistics.rake"
|
13
|
+
|
14
|
+
require "bundler/gem_tasks"
|
15
|
+
|
16
|
+
require "rake/testtask"
|
17
|
+
|
18
|
+
Rake::TestTask.new(:test) do |t|
|
19
|
+
t.libs << "test"
|
20
|
+
t.pattern = "test/**/*_test.rb"
|
21
|
+
t.verbose = false
|
22
|
+
end
|
23
|
+
|
24
|
+
task default: :test
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveInsights
|
4
|
+
class ApplicationController < ActionController::Base
|
5
|
+
protect_from_forgery with: :exception
|
6
|
+
|
7
|
+
around_action :setup_time_zone
|
8
|
+
before_action :set_date
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def set_date
|
13
|
+
@date =
|
14
|
+
if params[:date].present?
|
15
|
+
Date.parse(params[:date])
|
16
|
+
else
|
17
|
+
Date.current
|
18
|
+
end.all_day
|
19
|
+
end
|
20
|
+
|
21
|
+
def setup_time_zone(&block) # rubocop:disable Style/ArgumentsForwarding
|
22
|
+
Time.use_zone("Eastern Time (US & Canada)", &block) # rubocop:disable Style/ArgumentsForwarding
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveInsights
|
4
|
+
class ControllerPValuesController < ApplicationController
|
5
|
+
def index
|
6
|
+
@p50 = minutes.map{ |minute| [minute.pretty_started_at, minute.p50] }
|
7
|
+
@p95 = minutes.map{ |minute| [minute.pretty_started_at, minute.p95] }
|
8
|
+
@p99 = minutes.map{ |minute| [minute.pretty_started_at, minute.p99] }
|
9
|
+
end
|
10
|
+
|
11
|
+
def redirection
|
12
|
+
redirect_to controller_p_values_path(params[:date],
|
13
|
+
params[:formatted_controller])
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def minutes
|
19
|
+
@minutes ||=
|
20
|
+
ActiveInsights::Request.where(started_at: @date).
|
21
|
+
where(formatted_controller: params[:formatted_controller]).
|
22
|
+
group_by_minute.with_durations.select_started_at
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveInsights
|
4
|
+
class PValuesController < ApplicationController
|
5
|
+
def index
|
6
|
+
@p50 = minutes.map{ |minute| [minute.pretty_started_at, minute.p50] }
|
7
|
+
@p95 = minutes.map{ |minute| [minute.pretty_started_at, minute.p95] }
|
8
|
+
@p99 = minutes.map{ |minute| [minute.pretty_started_at, minute.p99] }
|
9
|
+
end
|
10
|
+
|
11
|
+
def redirection
|
12
|
+
redirect_to p_values_path(params[:date])
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def minutes
|
18
|
+
@minutes ||=
|
19
|
+
ActiveInsights::Request.where(started_at: @date).
|
20
|
+
group_by_minute.with_durations.select_started_at
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveInsights
|
4
|
+
class RequestsController < ApplicationController
|
5
|
+
def index
|
6
|
+
@requests =
|
7
|
+
ActiveInsights::Request.where(started_at: @date).
|
8
|
+
with_durations.select(:formatted_controller).
|
9
|
+
group(:formatted_controller).
|
10
|
+
sort_by { |model| model.durations.count(",") + 1 }.reverse
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveInsights
|
4
|
+
class RpmController < ApplicationController
|
5
|
+
def index
|
6
|
+
@minutes =
|
7
|
+
ActiveInsights::Request.where(started_at: @date).
|
8
|
+
group_by_minute.select("COUNT(id) AS rpm").select_started_at.
|
9
|
+
map { |minute| [minute.started_at.strftime("%-l:%M%P"), minute.rpm] }
|
10
|
+
end
|
11
|
+
|
12
|
+
def redirection
|
13
|
+
redirect_to rpm_path(params[:date])
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveInsights
|
4
|
+
module ApplicationHelper
|
5
|
+
def active_insights_importmap_tags(entry_point = "application")
|
6
|
+
importmap = ActiveInsights::Engine.importmap
|
7
|
+
|
8
|
+
safe_join [
|
9
|
+
javascript_inline_importmap_tag(importmap.to_json(resolver: self)),
|
10
|
+
javascript_importmap_module_preload_tags(importmap),
|
11
|
+
javascript_import_module_tag(entry_point)
|
12
|
+
], "\n"
|
13
|
+
end
|
14
|
+
|
15
|
+
def display_date(date)
|
16
|
+
if Date.current.year == date.year
|
17
|
+
date.strftime("%B %-d")
|
18
|
+
else
|
19
|
+
date.strftime("%B %-d, %Y")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def p50(data)
|
24
|
+
percentile_value(data, 0.5)
|
25
|
+
end
|
26
|
+
|
27
|
+
def p95(data)
|
28
|
+
percentile_value(data, 0.95)
|
29
|
+
end
|
30
|
+
|
31
|
+
def p99(data)
|
32
|
+
percentile_value(data, 0.99)
|
33
|
+
end
|
34
|
+
|
35
|
+
def per_minute(amount, duration)
|
36
|
+
(amount / duration.in_minutes).round(0)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def percentile_value(data, percentile)
|
42
|
+
value = data[(percentile * data.size).ceil - 1]
|
43
|
+
|
44
|
+
value&.round(1) || "N/A"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveInsights
|
4
|
+
class Request < ::ActiveInsights::Record
|
5
|
+
scope :with_durations, lambda {
|
6
|
+
case connection.adapter_name
|
7
|
+
when "SQLite", "Mysql2", "Mysql2Spatial", "Mysql2Rgeo", "Trilogy"
|
8
|
+
select("GROUP_CONCAT(duration) AS durations")
|
9
|
+
when "PostgreSQL"
|
10
|
+
select("STRING_AGG(CAST(duration AS varchar), ',') AS durations")
|
11
|
+
end
|
12
|
+
}
|
13
|
+
scope :group_by_minute, lambda {
|
14
|
+
case connection.adapter_name
|
15
|
+
when "SQLite"
|
16
|
+
group("strftime('%Y-%m-%d %H:%M:00 UTC', " \
|
17
|
+
"'active_insights_requests'.'started_at')")
|
18
|
+
when "Mysql2", "Mysql2Spatial", "Mysql2Rgeo", "Trilogy"
|
19
|
+
group("CONVERT_TZ(DATE_FORMAT(`active_insights_requests`.`started_at`" \
|
20
|
+
", '%Y-%m-%d %H:%i:00'), 'Etc/UTC', '+00:00')")
|
21
|
+
when "PostgreSQL"
|
22
|
+
group("DATE_TRUNC('minute', \"active_insights_requests\"." \
|
23
|
+
"\"started_at\"::timestamptz AT TIME ZONE 'Etc/UTC') " \
|
24
|
+
"AT TIME ZONE 'Etc/UTC'")
|
25
|
+
end
|
26
|
+
}
|
27
|
+
scope :select_started_at, lambda {
|
28
|
+
case connection.adapter_name
|
29
|
+
when "SQLite", "Mysql2", "Mysql2Spatial", "Mysql2Rgeo", "Trilogy"
|
30
|
+
select(:started_at)
|
31
|
+
when "PostgreSQL"
|
32
|
+
select("DATE_TRUNC('minute', \"active_insights_requests\"." \
|
33
|
+
"\"started_at\"::timestamptz AT TIME ZONE 'Etc/UTC') " \
|
34
|
+
"AT TIME ZONE 'Etc/UTC' as started_at")
|
35
|
+
end
|
36
|
+
}
|
37
|
+
|
38
|
+
def agony
|
39
|
+
parsed_durations.sum
|
40
|
+
end
|
41
|
+
|
42
|
+
def parsed_durations
|
43
|
+
return unless respond_to?(:durations)
|
44
|
+
|
45
|
+
@parsed_durations ||= durations.split(",").map(&:to_f).sort
|
46
|
+
end
|
47
|
+
|
48
|
+
def pretty_started_at
|
49
|
+
started_at.strftime("%-l:%M%P")
|
50
|
+
end
|
51
|
+
|
52
|
+
def p50
|
53
|
+
percentile_value(0.5)
|
54
|
+
end
|
55
|
+
|
56
|
+
def p95
|
57
|
+
percentile_value(0.95)
|
58
|
+
end
|
59
|
+
|
60
|
+
def p99
|
61
|
+
percentile_value(0.99)
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def percentile_value(percentile)
|
67
|
+
parsed_durations[(percentile * parsed_durations.size).ceil - 1].round(1)
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
<header>
|
2
|
+
<h1><%= params[:formatted_controller] %> metrics for <%= display_date(@date.first) %> (in ms)</h1>
|
3
|
+
<%= form_with url: active_insights.controller_p_values_redirection_path, method: :get do |f| %>
|
4
|
+
<%= f.date_field :date, max: Date.current, onchange: "this.form.submit()", value: @date.first.to_date %>
|
5
|
+
<% f.submit "submit", class: "hidden" %>
|
6
|
+
<% end %>
|
7
|
+
</header>
|
8
|
+
|
9
|
+
<div class="p-16px">
|
10
|
+
<%= line_chart [{ name: "p50", data: @p50 }, { name: "p95", data: @p95 }, { name: "p99", data: @p99 }], height: "80%", colors: ["rgb(255, 249, 216)", "green", "#C00"], library: {
|
11
|
+
plugins: { zoom: { zoom: { wheel: { enabled: false }, drag: { enabled: true, backgroundColor: 'rgba(225,225,225,0.5)' }, mode: 'x' } }, decimation: { enabled: true, algorithm: 'lttb' } },
|
12
|
+
elements: { point: { radius: 0 } },
|
13
|
+
scales: {
|
14
|
+
x: { autoSkip: false, display: false },
|
15
|
+
y: { grid: { display: false }, ticks: { color: "white" }, min: (@p50.map(&:second).min * 0.98).ceil, max: (@p99.map(&:second).max) }
|
16
|
+
} } %>
|
17
|
+
</div>
|
18
|
+
|
19
|
+
<script>
|
20
|
+
document.addEventListener('keydown',(event) => {
|
21
|
+
const chart = Chartkick.charts[Object.keys(Chartkick.charts)[0]].getChartObject()
|
22
|
+
if (event.key === 'Escape' || event.key === 'Esc') {
|
23
|
+
chart.resetZoom()
|
24
|
+
}
|
25
|
+
});
|
26
|
+
</script>
|
@@ -0,0 +1,26 @@
|
|
1
|
+
<header>
|
2
|
+
<h1>Response Metrics for <%= display_date(@date.first) %> (in ms)</h1>
|
3
|
+
<%= form_with url: active_insights.p_values_redirection_path, method: :get do |f| %>
|
4
|
+
<%= f.date_field :date, max: Date.current, onchange: "this.form.submit()", value: @date.first.to_date %>
|
5
|
+
<% f.submit "submit", class: "hidden" %>
|
6
|
+
<% end %>
|
7
|
+
</header>
|
8
|
+
|
9
|
+
<div class="p-16px">
|
10
|
+
<%= line_chart [{ name: "p50", data: @p50 }, { name: "p95", data: @p95 }, { name: "p99", data: @p99 }], height: "80%", colors: ["rgb(255, 249, 216)", "green", "#C00"], library: {
|
11
|
+
plugins: { zoom: { zoom: { wheel: { enabled: false }, drag: { enabled: true, backgroundColor: 'rgba(225,225,225,0.5)' }, mode: 'x' } }, decimation: { enabled: true, algorithm: 'lttb' } },
|
12
|
+
elements: { point: { radius: 0 } },
|
13
|
+
scales: {
|
14
|
+
x: { autoSkip: false, display: false },
|
15
|
+
y: { grid: { display: false }, ticks: { color: "white" }, min: (@p50.map(&:second).min * 0.98).ceil, max: (@p99.map(&:second).max) }
|
16
|
+
} } %>
|
17
|
+
</div>
|
18
|
+
|
19
|
+
<script>
|
20
|
+
document.addEventListener('keydown',(event) => {
|
21
|
+
const chart = Chartkick.charts[Object.keys(Chartkick.charts)[0]].getChartObject()
|
22
|
+
if (event.key === 'Escape' || event.key === 'Esc') {
|
23
|
+
chart.resetZoom()
|
24
|
+
}
|
25
|
+
});
|
26
|
+
</script>
|
@@ -0,0 +1,54 @@
|
|
1
|
+
<header>
|
2
|
+
<h1>Metrics for <%= display_date(@date.first) %></h1>
|
3
|
+
<%= form_with url: active_insights.requests_path, method: :get do |f| %>
|
4
|
+
<%= f.date_field :date, max: Date.current, onchange: "this.form.submit()", value: @date.first.to_date %>
|
5
|
+
<% f.submit "submit", class: "hidden" %>
|
6
|
+
<% end %>
|
7
|
+
</header>
|
8
|
+
|
9
|
+
<div class="pl-30px pt-30px flex flex-row justify-around font-size-30">
|
10
|
+
<% @requests.flat_map(&:parsed_durations).tap do |durations| %>
|
11
|
+
<%= link_to rpm_path(@date.first.to_date), class: "flex flex-col justify-center items-center no-underline" do %>
|
12
|
+
<div><%= per_minute(durations.size, 24.hours) %></div>
|
13
|
+
<b>RPM</b>
|
14
|
+
<% end %>
|
15
|
+
|
16
|
+
<%= link_to p_values_path(@date.first.to_date), class: "flex flex-col justify-center items-center no-underline" do %>
|
17
|
+
<div><%= p50(durations) %> ms</div>
|
18
|
+
<b>p50</b>
|
19
|
+
<% end %>
|
20
|
+
|
21
|
+
<%= link_to p_values_path(@date.first.to_date), class: "flex flex-col justify-center items-center no-underline" do %>
|
22
|
+
<div><%= p95(durations) %> ms</div>
|
23
|
+
<b>p95</b>
|
24
|
+
<% end %>
|
25
|
+
|
26
|
+
<%= link_to p_values_path(@date.first.to_date), class: "flex flex-col justify-center items-center no-underline" do %>
|
27
|
+
<div><%= p99(durations) %> ms</div>
|
28
|
+
<b>p99</b>
|
29
|
+
<% end %>
|
30
|
+
<% end %>
|
31
|
+
</div>
|
32
|
+
|
33
|
+
<table>
|
34
|
+
<thead>
|
35
|
+
<tr>
|
36
|
+
<th>Controller</th>
|
37
|
+
<th>RPM</th>
|
38
|
+
<th>p50</th>
|
39
|
+
<th>p95</th>
|
40
|
+
<th>p99</th>
|
41
|
+
</tr>
|
42
|
+
</thead>
|
43
|
+
<tbody>
|
44
|
+
<% @requests.sort_by(&:agony).reverse.each do |model| %>
|
45
|
+
<tr>
|
46
|
+
<td><%= link_to model.formatted_controller, controller_p_values_path(@date.first.to_date, model.formatted_controller) %></td>
|
47
|
+
<td><%= per_minute(model.parsed_durations.size, 24.hours) %></td>
|
48
|
+
<td><%= model.p50 %> ms</td>
|
49
|
+
<td><%= model.p95 %> ms</td>
|
50
|
+
<td><%= model.p99 %> ms</td>
|
51
|
+
</tr>
|
52
|
+
<% end %>
|
53
|
+
</tbody>
|
54
|
+
</table>
|
@@ -0,0 +1,26 @@
|
|
1
|
+
<header>
|
2
|
+
<h1>RPM Metrics for <%= display_date(@date.first) %></h1>
|
3
|
+
<%= form_with url: active_insights.rpm_redirection_path, method: :get do |f| %>
|
4
|
+
<%= f.date_field :date, max: Date.current, onchange: "this.form.submit()", value: @date.first.to_date %>
|
5
|
+
<% f.submit "submit", class: "hidden" %>
|
6
|
+
<% end %>
|
7
|
+
</header>
|
8
|
+
|
9
|
+
<div class="p-16px">
|
10
|
+
<%= column_chart @minutes, height: "80%", colors: ["#C00"], library: {
|
11
|
+
borderSkipped: true, barPercentage: 1, categoryPercentage: 1,
|
12
|
+
plugins: { zoom: { zoom: { wheel: { enabled: false }, drag: { enabled: true, backgroundColor: 'rgba(225,225,225,0.5)' }, mode: 'x' } } },
|
13
|
+
scales: {
|
14
|
+
x: { barPercentage: 1.0, autoSkip: false, display: false },
|
15
|
+
y: { grid: { display: false }, ticks: { color: "white" }, min: (@minutes.map(&:second).min * 0.98).ceil, max: (@minutes.map(&:second).max) }
|
16
|
+
} } %>
|
17
|
+
</div>
|
18
|
+
|
19
|
+
<script>
|
20
|
+
document.addEventListener('keydown',(event) => {
|
21
|
+
const chart = Chartkick.charts[Object.keys(Chartkick.charts)[0]].getChartObject()
|
22
|
+
if (event.key === 'Escape' || event.key === 'Esc') {
|
23
|
+
chart.resetZoom()
|
24
|
+
}
|
25
|
+
});
|
26
|
+
</script>
|
@@ -0,0 +1,345 @@
|
|
1
|
+
<html lang="en">
|
2
|
+
<head>
|
3
|
+
<meta charset="utf-8" />
|
4
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
5
|
+
<meta name="turbo-visit-control" content="reload">
|
6
|
+
<%= csrf_meta_tags %>
|
7
|
+
<%= csp_meta_tag %>
|
8
|
+
<%= active_insights_importmap_tags %>
|
9
|
+
|
10
|
+
<title>Active Insights</title>
|
11
|
+
<style>
|
12
|
+
body {
|
13
|
+
background-color: #FAFAFA;
|
14
|
+
color: #333;
|
15
|
+
color-scheme: light dark;
|
16
|
+
supported-color-schemes: light dark;
|
17
|
+
margin: 0px;
|
18
|
+
}
|
19
|
+
|
20
|
+
body, p, ol, ul, td, input {
|
21
|
+
font-family: helvetica, verdana, arial, sans-serif;
|
22
|
+
font-size: 15px;
|
23
|
+
line-height: 18px;
|
24
|
+
}
|
25
|
+
|
26
|
+
form {
|
27
|
+
margin-bottom: 0px;
|
28
|
+
}
|
29
|
+
|
30
|
+
pre {
|
31
|
+
font-size: 11px;
|
32
|
+
white-space: pre-wrap;
|
33
|
+
}
|
34
|
+
|
35
|
+
pre.box {
|
36
|
+
border: 1px solid #EEE;
|
37
|
+
padding: 10px;
|
38
|
+
margin: 0px;
|
39
|
+
width: 958px;
|
40
|
+
}
|
41
|
+
|
42
|
+
header {
|
43
|
+
color: #F0F0F0;
|
44
|
+
background: #C00;
|
45
|
+
padding: 0.5em 1.5em;
|
46
|
+
display: flex;
|
47
|
+
flex-direction: row;
|
48
|
+
justify-content: space-between;
|
49
|
+
align-items: center;
|
50
|
+
}
|
51
|
+
|
52
|
+
h1 {
|
53
|
+
overflow-wrap: break-word;
|
54
|
+
margin: 0.2em 0;
|
55
|
+
line-height: 1.1em;
|
56
|
+
font-size: 2em;
|
57
|
+
}
|
58
|
+
|
59
|
+
h2 {
|
60
|
+
color: #C00;
|
61
|
+
line-height: 25px;
|
62
|
+
}
|
63
|
+
|
64
|
+
code.traces {
|
65
|
+
font-size: 11px;
|
66
|
+
}
|
67
|
+
|
68
|
+
.response-heading, .request-heading {
|
69
|
+
margin-top: 30px;
|
70
|
+
}
|
71
|
+
|
72
|
+
.exception-message {
|
73
|
+
padding: 8px 0;
|
74
|
+
}
|
75
|
+
|
76
|
+
.exception-message .message {
|
77
|
+
margin-bottom: 8px;
|
78
|
+
line-height: 25px;
|
79
|
+
font-size: 1.5em;
|
80
|
+
font-weight: bold;
|
81
|
+
color: #C00;
|
82
|
+
}
|
83
|
+
|
84
|
+
.details {
|
85
|
+
border: 1px solid #D0D0D0;
|
86
|
+
border-radius: 4px;
|
87
|
+
margin: 1em 0px;
|
88
|
+
display: block;
|
89
|
+
max-width: 978px;
|
90
|
+
}
|
91
|
+
|
92
|
+
.summary {
|
93
|
+
padding: 8px 15px;
|
94
|
+
border-bottom: 1px solid #D0D0D0;
|
95
|
+
display: block;
|
96
|
+
}
|
97
|
+
|
98
|
+
a.summary {
|
99
|
+
color: #F0F0F0;
|
100
|
+
text-decoration: none;
|
101
|
+
background: #C52F24;
|
102
|
+
border-bottom: none;
|
103
|
+
}
|
104
|
+
|
105
|
+
.details pre {
|
106
|
+
margin: 5px;
|
107
|
+
border: none;
|
108
|
+
}
|
109
|
+
|
110
|
+
#container {
|
111
|
+
box-sizing: border-box;
|
112
|
+
width: 100%;
|
113
|
+
padding: 0 1.5em;
|
114
|
+
}
|
115
|
+
|
116
|
+
.source * {
|
117
|
+
margin: 0px;
|
118
|
+
padding: 0px;
|
119
|
+
}
|
120
|
+
|
121
|
+
.source {
|
122
|
+
border: 1px solid #D9D9D9;
|
123
|
+
background: #ECECEC;
|
124
|
+
max-width: 978px;
|
125
|
+
}
|
126
|
+
|
127
|
+
.source pre {
|
128
|
+
padding: 10px 0px;
|
129
|
+
border: none;
|
130
|
+
}
|
131
|
+
|
132
|
+
.source .data {
|
133
|
+
font-size: 80%;
|
134
|
+
overflow: auto;
|
135
|
+
background-color: #FFF;
|
136
|
+
}
|
137
|
+
|
138
|
+
.info {
|
139
|
+
padding: 0.5em;
|
140
|
+
}
|
141
|
+
|
142
|
+
.source .data .line_numbers {
|
143
|
+
background-color: #ECECEC;
|
144
|
+
color: #555;
|
145
|
+
padding: 1em .5em;
|
146
|
+
border-right: 1px solid #DDD;
|
147
|
+
text-align: right;
|
148
|
+
}
|
149
|
+
|
150
|
+
.line {
|
151
|
+
padding-left: 10px;
|
152
|
+
white-space: pre;
|
153
|
+
}
|
154
|
+
|
155
|
+
.line:hover {
|
156
|
+
background-color: #F6F6F6;
|
157
|
+
}
|
158
|
+
|
159
|
+
.line.active {
|
160
|
+
background-color: #FCC;
|
161
|
+
}
|
162
|
+
|
163
|
+
.error_highlight {
|
164
|
+
display: inline-block;
|
165
|
+
background-color: #FF9;
|
166
|
+
text-decoration: #F00 wavy underline;
|
167
|
+
}
|
168
|
+
|
169
|
+
.error_highlight_tip {
|
170
|
+
color: #666;
|
171
|
+
padding: 2px 2px;
|
172
|
+
font-size: 10px;
|
173
|
+
}
|
174
|
+
|
175
|
+
.button_to {
|
176
|
+
display: inline-block;
|
177
|
+
margin-top: 0.75em;
|
178
|
+
margin-bottom: 0.75em;
|
179
|
+
}
|
180
|
+
|
181
|
+
.hidden {
|
182
|
+
display: none;
|
183
|
+
}
|
184
|
+
|
185
|
+
.correction {
|
186
|
+
list-style-type: none;
|
187
|
+
}
|
188
|
+
|
189
|
+
input[type="submit"] {
|
190
|
+
color: white;
|
191
|
+
background-color: #C00;
|
192
|
+
border: none;
|
193
|
+
border-radius: 12px;
|
194
|
+
box-shadow: 0 3px #F99;
|
195
|
+
font-size: 13px;
|
196
|
+
font-weight: bold;
|
197
|
+
margin: 0;
|
198
|
+
padding: 10px 18px;
|
199
|
+
cursor: pointer;
|
200
|
+
-webkit-appearance: none;
|
201
|
+
}
|
202
|
+
input[type="submit"]:focus,
|
203
|
+
input[type="submit"]:hover {
|
204
|
+
opacity: 0.8;
|
205
|
+
}
|
206
|
+
input[type="submit"]:active {
|
207
|
+
box-shadow: 0 2px #F99;
|
208
|
+
transform: translateY(1px)
|
209
|
+
}
|
210
|
+
|
211
|
+
a { color: #980905; }
|
212
|
+
a:visited { color: #666; }
|
213
|
+
a.trace-frames {
|
214
|
+
color: #666;
|
215
|
+
overflow-wrap: break-word;
|
216
|
+
}
|
217
|
+
a:hover, a.trace-frames.selected { color: #C00; }
|
218
|
+
a.summary:hover { color: #FFF; }
|
219
|
+
|
220
|
+
@media (prefers-color-scheme: dark) {
|
221
|
+
body {
|
222
|
+
background-color: #222;
|
223
|
+
color: #ECECEC;
|
224
|
+
}
|
225
|
+
|
226
|
+
.details, .summary {
|
227
|
+
border-color: #666;
|
228
|
+
}
|
229
|
+
|
230
|
+
.source {
|
231
|
+
border-color: #555;
|
232
|
+
background-color: #333;
|
233
|
+
}
|
234
|
+
|
235
|
+
.source .data {
|
236
|
+
background: #444;
|
237
|
+
}
|
238
|
+
|
239
|
+
.source .data .line_numbers {
|
240
|
+
background: #333;
|
241
|
+
border-color: #222;
|
242
|
+
}
|
243
|
+
|
244
|
+
.line:hover {
|
245
|
+
background: #666;
|
246
|
+
}
|
247
|
+
|
248
|
+
.line.active {
|
249
|
+
background-color: #900;
|
250
|
+
}
|
251
|
+
|
252
|
+
.error_highlight {
|
253
|
+
color: #333;
|
254
|
+
}
|
255
|
+
|
256
|
+
input[type="submit"] {
|
257
|
+
box-shadow: 0 3px #800;
|
258
|
+
}
|
259
|
+
input[type="submit"]:active {
|
260
|
+
box-shadow: 0 2px #800;
|
261
|
+
}
|
262
|
+
|
263
|
+
a { color: #C00; }
|
264
|
+
}
|
265
|
+
|
266
|
+
table {
|
267
|
+
margin: 0;
|
268
|
+
border-collapse: collapse;
|
269
|
+
word-wrap:break-word;
|
270
|
+
table-layout: auto;
|
271
|
+
width: 100%;
|
272
|
+
margin-top: 50px;
|
273
|
+
}
|
274
|
+
|
275
|
+
table thead tr {
|
276
|
+
border-bottom: 2px solid #ddd;
|
277
|
+
}
|
278
|
+
|
279
|
+
table th {
|
280
|
+
padding-left: 30px;
|
281
|
+
text-align: left;
|
282
|
+
}
|
283
|
+
|
284
|
+
table tbody tr {
|
285
|
+
border-bottom: 1px solid #ddd;
|
286
|
+
}
|
287
|
+
|
288
|
+
table tbody tr:nth-child(odd) {
|
289
|
+
background: #f2f2f2;
|
290
|
+
}
|
291
|
+
|
292
|
+
table td {
|
293
|
+
padding: 4px 30px;
|
294
|
+
}
|
295
|
+
|
296
|
+
@media (prefers-color-scheme: dark) {
|
297
|
+
table tbody tr:nth-child(odd) {
|
298
|
+
background: #282828;
|
299
|
+
}
|
300
|
+
}
|
301
|
+
|
302
|
+
.pl-30px {
|
303
|
+
padding-left: 30px;
|
304
|
+
}
|
305
|
+
|
306
|
+
.pt-30px {
|
307
|
+
padding-top: 30px;
|
308
|
+
}
|
309
|
+
|
310
|
+
.flex {
|
311
|
+
display: flex;
|
312
|
+
}
|
313
|
+
|
314
|
+
.flex-col {
|
315
|
+
flex-direction: column;
|
316
|
+
}
|
317
|
+
.flex-row {
|
318
|
+
flex-direction: row;
|
319
|
+
}
|
320
|
+
.justify-around {
|
321
|
+
justify-content: space-around;
|
322
|
+
}
|
323
|
+
|
324
|
+
.justify-center {
|
325
|
+
justify-content: center;
|
326
|
+
}
|
327
|
+
.items-center {
|
328
|
+
align-items: center;
|
329
|
+
}
|
330
|
+
.font-size-30 {
|
331
|
+
font-size: 30px;
|
332
|
+
line-height: 30px;
|
333
|
+
}
|
334
|
+
.no-underline {
|
335
|
+
text-decoration: none;
|
336
|
+
}
|
337
|
+
.p-16px {
|
338
|
+
padding: 16px;
|
339
|
+
}
|
340
|
+
|
341
|
+
<%= yield :style %>
|
342
|
+
</style>
|
343
|
+
</head>
|
344
|
+
<body><%= yield %></body>
|
345
|
+
</html>
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_insights"
|
4
|
+
|
5
|
+
ActiveInsights::Engine.importmap.draw do
|
6
|
+
pin "application", to: "active_insights/application.js"
|
7
|
+
|
8
|
+
pin "chartkick", to: "chartkick.js"
|
9
|
+
pin "Chart.bundle", to: "Chart.bundle.js"
|
10
|
+
|
11
|
+
pin "chartjs-plugin-zoom", to: "https://ga.jspm.io/npm:chartjs-plugin-zoom@2.0.1/dist/chartjs-plugin-zoom.esm.js"
|
12
|
+
pin "hammerjs", to: "https://ga.jspm.io/npm:hammerjs@2.0.8/hammer.js"
|
13
|
+
pin "@kurkle/color", to: "https://ga.jspm.io/npm:@kurkle/color@0.3.2/dist/color.esm.js"
|
14
|
+
pin "chart.js/helpers", to: "https://ga.jspm.io/npm:chart.js@4.4.1/helpers/helpers.js"
|
15
|
+
end
|
data/config/routes.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
ActiveInsights::Engine.routes.draw do
|
4
|
+
resources :requests, only: %i(index)
|
5
|
+
get "/requests/:date", to: "requests#index"
|
6
|
+
get "/requests/rpm/redirection", to: "rpm#redirection", as: :rpm_redirection
|
7
|
+
get "/requests/:date/rpm", to: "rpm#index", as: :rpm
|
8
|
+
|
9
|
+
get "/requests/p_values/redirection", to: "p_values#redirection",
|
10
|
+
as: :p_values_redirection
|
11
|
+
get "/requests/:date/p_values", to: "p_values#index", as: :p_values
|
12
|
+
get "/requests/:date/:formatted_controller/p_values",
|
13
|
+
to: "controller_p_values#index", as: :controller_p_values
|
14
|
+
get "/requests/:formatted_controller/p_values/redirection",
|
15
|
+
to: "controller_p_values#redirection",
|
16
|
+
as: :controller_p_values_redirection
|
17
|
+
|
18
|
+
root "requests#index"
|
19
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class ActiveInsightsRequest < ActiveRecord::Migration[7.1]
|
4
|
+
def change # rubocop:disable Metrics/AbcSize
|
5
|
+
create_table :active_insights_requests, if_not_exists: true do |t|
|
6
|
+
t.string :controller
|
7
|
+
t.string :action
|
8
|
+
t.string :format
|
9
|
+
t.string :http_method
|
10
|
+
t.text :path
|
11
|
+
t.integer :status
|
12
|
+
t.float :view_runtime
|
13
|
+
t.float :db_runtime
|
14
|
+
t.datetime :started_at
|
15
|
+
t.datetime :finished_at
|
16
|
+
t.string :uuid
|
17
|
+
t.float :duration
|
18
|
+
case connection.adapter_name
|
19
|
+
when "Mysql2", "Mysql2Spatial", "Mysql2Rgeo", "Trilogy"
|
20
|
+
t.virtual :formatted_controller, type: :string, as: "CONCAT(controller, '#', action)", stored: true
|
21
|
+
else
|
22
|
+
t.virtual :formatted_controller, type: :string, as: "controller || '#'|| action", stored: true
|
23
|
+
end
|
24
|
+
|
25
|
+
t.index :started_at
|
26
|
+
t.index %i(started_at duration)
|
27
|
+
t.index %i(started_at formatted_controller)
|
28
|
+
|
29
|
+
t.timestamps
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveInsights
|
4
|
+
class Engine < ::Rails::Engine
|
5
|
+
isolate_namespace ActiveInsights
|
6
|
+
|
7
|
+
def self.importmap
|
8
|
+
@importmap ||=
|
9
|
+
Importmap::Map.new.tap do |mapping|
|
10
|
+
mapping.draw(Engine.root.join("config/importmap.rb"))
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
initializer "active_insights.importmap", before: "importmap" do |app|
|
15
|
+
app.config.importmap.paths <<
|
16
|
+
Engine.root.join("config/initializers/importmap.rb")
|
17
|
+
end
|
18
|
+
|
19
|
+
initializer "active_insights.subscriber" do |_app|
|
20
|
+
ActiveSupport::Notifications.
|
21
|
+
subscribe("process_action.action_controller") do |_name,
|
22
|
+
started, finished, unique_id, payload|
|
23
|
+
Thread.new do
|
24
|
+
ActiveRecord::Base.connection_pool.with_connection do
|
25
|
+
ActiveInsights::Request.create!(
|
26
|
+
started_at: started, finished_at: finished, uuid: unique_id,
|
27
|
+
duration: (finished - started) * 1000.0,
|
28
|
+
controller: payload[:controller],
|
29
|
+
action: payload[:action], format: payload[:format],
|
30
|
+
http_method: payload[:method], status: payload[:status],
|
31
|
+
view_runtime: payload[:view_runtime],
|
32
|
+
db_runtime: payload[:db_runtime]
|
33
|
+
)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rubystats"
|
4
|
+
|
5
|
+
module ActiveInsights
|
6
|
+
class Seeder
|
7
|
+
def initialize(date, rpm, p50, p95, p99)
|
8
|
+
@date = date
|
9
|
+
@rpm = rpm
|
10
|
+
@p50 = p50
|
11
|
+
@p95 = p95
|
12
|
+
@p99 = p99
|
13
|
+
end
|
14
|
+
|
15
|
+
def seed
|
16
|
+
ActiveInsights::Request.insert_all(seed_attributes)
|
17
|
+
end
|
18
|
+
|
19
|
+
def find_percentile(sorted_data, percentile)
|
20
|
+
sorted_data[(percentile * sorted_data.length).ceil - 1]
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
attr_reader :date, :rpm, :p50, :p95, :p99
|
26
|
+
|
27
|
+
def seed_attributes
|
28
|
+
24.times.flat_map do |hour|
|
29
|
+
60.times.flat_map do |min|
|
30
|
+
response_times.map { |duration| attributes(hour, min, duration) }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def response_times
|
36
|
+
Array.new(rpm) do
|
37
|
+
p50 + (beta_distribution.rng * (p95 - p50))
|
38
|
+
end.select { |time| time <= p99 }
|
39
|
+
end
|
40
|
+
|
41
|
+
def beta_distribution
|
42
|
+
@beta_distribution ||= Rubystats::BetaDistribution.new(2, 5)
|
43
|
+
end
|
44
|
+
|
45
|
+
def sample_controller
|
46
|
+
["AppsController#show", "OrganizationsController#show",
|
47
|
+
"AgentController#create", "AppComponentsController#index",
|
48
|
+
"BadgesController#show", "ContactController#create",
|
49
|
+
"AppsController#update"].sample.split("#")
|
50
|
+
end
|
51
|
+
|
52
|
+
def attributes(hour, min, duration)
|
53
|
+
sample_controller.then do |controller, action|
|
54
|
+
started_at = date.dup.to_time.change(hour:, min:)
|
55
|
+
|
56
|
+
default_attributes.merge(controller:, action:, duration:, started_at:,
|
57
|
+
finished_at: started_at + (duration / 1000.0))
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def default_attributes
|
62
|
+
{ created_at: Time.current, updated_at: Time.current, format: :html,
|
63
|
+
http_method: :get, uuid: SecureRandom.uuid }
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class ActiveInsights::InstallGenerator < Rails::Generators::Base
|
4
|
+
def add_route
|
5
|
+
route "mount ActiveInsights::Engine => \"/insights\""
|
6
|
+
end
|
7
|
+
|
8
|
+
def create_migrations
|
9
|
+
rails_command "active_insights:install:migrations", inline: true
|
10
|
+
end
|
11
|
+
end
|
metadata
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: activeinsights
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Nick Pezza
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-01-17 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: chartkick
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: importmap-rails
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rails
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '7.1'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '7.1'
|
55
|
+
description:
|
56
|
+
email:
|
57
|
+
- pezza@hey.com
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- README.md
|
63
|
+
- Rakefile
|
64
|
+
- app/assets/javascripts/active_insights/application.js
|
65
|
+
- app/controllers/active_insights/application_controller.rb
|
66
|
+
- app/controllers/active_insights/controller_p_values_controller.rb
|
67
|
+
- app/controllers/active_insights/p_values_controller.rb
|
68
|
+
- app/controllers/active_insights/requests_controller.rb
|
69
|
+
- app/controllers/active_insights/rpm_controller.rb
|
70
|
+
- app/helpers/active_insights/application_helper.rb
|
71
|
+
- app/models/active_insights/record.rb
|
72
|
+
- app/models/active_insights/request.rb
|
73
|
+
- app/views/active_insights/controller_p_values/index.html.erb
|
74
|
+
- app/views/active_insights/p_values/index.html.erb
|
75
|
+
- app/views/active_insights/requests/index.html.erb
|
76
|
+
- app/views/active_insights/rpm/index.html.erb
|
77
|
+
- app/views/layouts/active_insights/application.html.erb
|
78
|
+
- config/initializers/importmap.rb
|
79
|
+
- config/routes.rb
|
80
|
+
- db/migrate/20240111225806_active_insights_request.rb
|
81
|
+
- lib/active_insights.rb
|
82
|
+
- lib/active_insights/engine.rb
|
83
|
+
- lib/active_insights/seeder.rb
|
84
|
+
- lib/active_insights/version.rb
|
85
|
+
- lib/activeinsights.rb
|
86
|
+
- lib/generators/active_insights/install/install_generator.rb
|
87
|
+
homepage: https://github.com/npezza93/activeinsights
|
88
|
+
licenses:
|
89
|
+
- MIT
|
90
|
+
metadata:
|
91
|
+
rubygems_mfa_required: 'true'
|
92
|
+
post_install_message:
|
93
|
+
rdoc_options: []
|
94
|
+
require_paths:
|
95
|
+
- lib
|
96
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
97
|
+
requirements:
|
98
|
+
- - ">="
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
version: 3.1.0
|
101
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
102
|
+
requirements:
|
103
|
+
- - ">="
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
version: '0'
|
106
|
+
requirements: []
|
107
|
+
rubygems_version: 3.5.3
|
108
|
+
signing_key:
|
109
|
+
specification_version: 4
|
110
|
+
summary: Rails performance tracking
|
111
|
+
test_files: []
|