activeinsights 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
![screenshot 1](https://github.com/npezza93/activeinsights/blob/main/.github/screenshot1.png)
|
12
|
+
![screenshot 2](https://github.com/npezza93/activeinsights/blob/main/.github/screenshot2.png)
|
13
|
+
![screenshot 3](https://github.com/npezza93/activeinsights/blob/main/.github/screenshot3.png)
|
14
|
+
![screenshot 4](https://github.com/npezza93/activeinsights/blob/main/.github/screenshot4.png)
|
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: []
|