builder_apm 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +1 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +6 -0
  5. data/CODE_OF_CONDUCT.md +74 -0
  6. data/Gemfile +17 -0
  7. data/Gemfile.lock +196 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +50 -0
  10. data/Rakefile +28 -0
  11. data/app/controllers/builder_apm/dashboard_controller.rb +8 -0
  12. data/app/controllers/builder_apm/error_requests_controller.rb +8 -0
  13. data/app/controllers/builder_apm/n_plus_one_controller.rb +8 -0
  14. data/app/controllers/builder_apm/recent_requests_controller.rb +8 -0
  15. data/app/controllers/builder_apm/request_analysis_controller.rb +8 -0
  16. data/app/controllers/builder_apm/request_data_controller.rb +41 -0
  17. data/app/controllers/builder_apm/request_details_controller.rb +9 -0
  18. data/app/controllers/builder_apm/slow_requests_controller.rb +8 -0
  19. data/app/controllers/builder_apm/wip_controller.rb +8 -0
  20. data/app/views/builder_apm/css/_dark.html.erb +119 -0
  21. data/app/views/builder_apm/css/_main.html.erb +268 -0
  22. data/app/views/builder_apm/dashboard/index.html.erb +10 -0
  23. data/app/views/builder_apm/error_requests/index.html.erb +23 -0
  24. data/app/views/builder_apm/js/_compress.html.erb +93 -0
  25. data/app/views/builder_apm/js/_dashboard.html.erb +199 -0
  26. data/app/views/builder_apm/js/_data_fetcher.html.erb +254 -0
  27. data/app/views/builder_apm/js/_error_requests.html.erb +65 -0
  28. data/app/views/builder_apm/js/_lzma.html.erb +2670 -0
  29. data/app/views/builder_apm/js/_n_plus_one.html.erb +79 -0
  30. data/app/views/builder_apm/js/_recent_requests.html.erb +82 -0
  31. data/app/views/builder_apm/js/_request_analysis.html.erb +77 -0
  32. data/app/views/builder_apm/js/_request_details.html.erb +204 -0
  33. data/app/views/builder_apm/js/_slow_requests.html.erb +74 -0
  34. data/app/views/builder_apm/n_plus_one/index.html.erb +21 -0
  35. data/app/views/builder_apm/recent_requests/index.html.erb +21 -0
  36. data/app/views/builder_apm/request_analysis/index.html.erb +24 -0
  37. data/app/views/builder_apm/request_details/index.html.erb +7 -0
  38. data/app/views/builder_apm/shared/_footer.html.erb +3 -0
  39. data/app/views/builder_apm/shared/_header.html.erb +55 -0
  40. data/app/views/builder_apm/slow_requests/index.html.erb +21 -0
  41. data/app/views/builder_apm/wip/index.html.erb +5 -0
  42. data/bin/console +14 -0
  43. data/bin/setup +8 -0
  44. data/builder_apm.gemspec +23 -0
  45. data/config/routes.rb +12 -0
  46. data/lib/builder_apm/configuration.rb +15 -0
  47. data/lib/builder_apm/controllers/instrumenter.rb +88 -0
  48. data/lib/builder_apm/engine.rb +17 -0
  49. data/lib/builder_apm/methods/instrumenter.rb +79 -0
  50. data/lib/builder_apm/middleware/timing.rb +56 -0
  51. data/lib/builder_apm/models/instrumenter.rb +82 -0
  52. data/lib/builder_apm/railtie.rb +9 -0
  53. data/lib/builder_apm/redis_client.rb +11 -0
  54. data/lib/builder_apm/version.rb +3 -0
  55. data/lib/builder_apm.rb +22 -0
  56. data/lib/generators/builder_apm/install_generator.rb +21 -0
  57. data/lib/generators/builder_apm/templates/builder_apm_config.rb +6 -0
  58. data/lib/generators/builder_apm/templates/create_builder_apm_requests.rb +21 -0
  59. data/lib/generators/builder_apm/templates/create_builder_apm_sql_queries.rb +17 -0
  60. metadata +135 -0
@@ -0,0 +1,79 @@
1
+ <script>
2
+ $(document).ready(function() {
3
+ fetchDataAndUpdateStorage(function(updatedData) {
4
+ renderTable(updatedData);
5
+ });
6
+
7
+ setInterval(function() {
8
+ var autoUpdate = $('#autoUpdate').is(':checked');
9
+ if (autoUpdate) {
10
+ fetchDataAndUpdateStorage(function(updatedData) {
11
+ renderTable(updatedData);
12
+ });
13
+ }
14
+ }, 5000);
15
+
16
+ addSortingClick();
17
+ });
18
+
19
+ function renderTable(data, target = null) {
20
+ data = detectNPlusOne(data);
21
+ data = sortDataBy(current_sort_field, data);
22
+
23
+ // Get a reference to the table body
24
+ var tableBody = target || $('table tbody');
25
+
26
+ // Clear the table body
27
+ tableBody.empty();
28
+
29
+ // Create new table rows for each data item
30
+ data.forEach(function(item) {
31
+ var row = $('<tr>');
32
+
33
+ $('<td>').text(new Date(item['start_time']).toLocaleString()).appendTo(row);
34
+ $('<td>').text(item['controller'] + '#' + item['action']).appendTo(row);
35
+ $('<td>').text(item['status']).appendTo(row);
36
+ $('<td>').append(renderDuration(item['duration'])).appendTo(row);
37
+ $('<td>').append(renderDuration(item['db_runtime'])).appendTo(row);
38
+ $('<td>').append(renderDuration(item['view_runtime'])).appendTo(row);
39
+ // Action column
40
+ var actionTd = $('<td>');
41
+ var actionButton = $('<button>').text('Details');
42
+ actionButton.on('click', function() {
43
+ // Replace 'your-details-url' with the actual URL where the details are to be fetched.
44
+ // It's assumed the ID is required as a URL parameter, modify as per your requirements.
45
+ window.location.href = '<%= request_details_path %>?request_id=' + item['request_id'];
46
+ });
47
+ actionButton.appendTo(actionTd);
48
+ actionTd.appendTo(row);
49
+
50
+ // Append the row to the table body
51
+ tableBody.append(row);
52
+ });
53
+ }
54
+
55
+ function renderDuration(duration) {
56
+ var durationClass;
57
+ if (duration > 0 && duration < 500) {
58
+ durationClass = "duration-circle green-circle";
59
+ } else if (duration >= 500 && duration < 1000) {
60
+ durationClass = "duration-circle amber-circle";
61
+ } else if (duration >= 1000){
62
+ durationClass = "duration-circle red-circle";
63
+ }
64
+
65
+ var formattedDuration = duration == null || duration == 0 ? 'n/a' : parseFloat(parseFloat(duration).toFixed(3)) + 'ms';
66
+
67
+ // Create the duration span with the circle and the duration value
68
+ return $("<span>").addClass("duration").append(
69
+ $("<span>").addClass(durationClass),
70
+ `${formattedDuration}`
71
+ );
72
+ }
73
+
74
+ function detectNPlusOne(requests) {
75
+ tagQueriesWithNPlusOne(requests);
76
+ return getNPlusOneRequests(requests);
77
+ }
78
+
79
+ </script>
@@ -0,0 +1,82 @@
1
+ <script>
2
+
3
+ $(document).ready(function() {
4
+ fetchDataAndUpdateStorage(function(updatedData) {
5
+ // Render the table with the updated data
6
+ renderTable(updatedData);
7
+ });
8
+
9
+
10
+ setInterval(function() {
11
+ var autoUpdate = $('#autoUpdate').is(':checked');
12
+ if (autoUpdate) {
13
+ fetchDataAndUpdateStorage(function(updatedData) {
14
+ // Render the table with the updated data
15
+ renderTable(updatedData);
16
+ });
17
+ }
18
+ }, 5000);
19
+
20
+ addSortingClick();
21
+ });
22
+
23
+
24
+ function renderTable(data, target = null) {
25
+ data = sortDataBy(current_sort_field, data);
26
+
27
+ // Get a reference to the table body
28
+ var tableBody = target || $('table tbody');
29
+
30
+ // Clear the table body
31
+ tableBody.empty();
32
+
33
+ // Create new table rows for each data item
34
+ data.forEach(function(item) {
35
+ var row = $('<tr>');
36
+
37
+ $('<td>').text(new Date(item['start_time']).toLocaleString()).appendTo(row);
38
+ $('<td>').text(item['controller'] + '#' + item['action']).appendTo(row);
39
+ $('<td>').text(item['status']).appendTo(row);
40
+ $('<td>').append(renderDuration(item['real_duration_time'])).appendTo(row);
41
+ $('<td>').append(renderDuration(item['calc_db_runtime'])).appendTo(row);
42
+ $('<td>').append(renderDuration(item['view_runtime'])).appendTo(row);
43
+ // Action column
44
+ var actionTd = $('<td>');
45
+
46
+ if(item.stack && item.stack.length > 0) {
47
+
48
+ var actionButton = $('<button>').text('Details');
49
+ actionButton.on('click', function() {
50
+ // Replace 'your-details-url' with the actual URL where the details are to be fetched.
51
+ // It's assumed the ID is required as a URL parameter, modify as per your requirements.
52
+ window.location.href = '<%= request_details_path %>?request_id=' + item['request_id'];
53
+ });
54
+ actionButton.appendTo(actionTd);
55
+ }
56
+ actionTd.appendTo(row);
57
+
58
+ // Append the row to the table body
59
+ tableBody.append(row);
60
+ });
61
+ }
62
+
63
+ function renderDuration(duration) {
64
+ var durationClass;
65
+ if (duration > 0 && duration < 500) {
66
+ durationClass = "duration-circle green-circle";
67
+ } else if (duration >= 500 && duration < 1000) {
68
+ durationClass = "duration-circle amber-circle";
69
+ } else if (duration >= 1000){
70
+ durationClass = "duration-circle red-circle";
71
+ }
72
+
73
+ var formattedDuration = duration == null || duration == 0 ? 'n/a' : parseFloat(parseFloat(duration).toFixed(3)) + 'ms';
74
+
75
+ // Create the duration span with the circle and the duration value
76
+ return $("<span>").addClass("duration").append(
77
+ $("<span>").addClass(durationClass),
78
+ `${formattedDuration}`
79
+ );
80
+ }
81
+
82
+ </script>
@@ -0,0 +1,77 @@
1
+ <script>
2
+ $(document).ready(function() {
3
+ fetchDataAndUpdateStorage(function(updatedData) {
4
+ renderTable(updatedData);
5
+ });
6
+
7
+ setInterval(function() {
8
+ var autoUpdate = $('#autoUpdate').is(':checked');
9
+ if (autoUpdate) {
10
+ fetchDataAndUpdateStorage(function(updatedData) {
11
+ renderTable(updatedData);
12
+ });
13
+ }
14
+ }, 5000);
15
+
16
+ addSortingClick();
17
+ });
18
+
19
+ function renderTable(data, target = null) {
20
+ data = aggregateRequests(data);
21
+ data = sortDataBy(current_sort_field, data);
22
+
23
+ // Get a reference to the table body
24
+ var tableBody = target || $('table tbody');
25
+
26
+ // Clear the table body
27
+ tableBody.empty();
28
+
29
+ // Create new table rows for each data item
30
+ data.forEach(function(item) {
31
+ var row = $('<tr>');
32
+
33
+ $('<td>').text(item['controller']).appendTo(row);
34
+ $('<td>').text(item['path']).appendTo(row);
35
+ $('<td>').text(item['count']).appendTo(row);
36
+ $('<td>').append(renderDuration(item['averageDuration'])).appendTo(row);
37
+ $('<td>').append(renderDuration(item['averageDbRuntime'])).appendTo(row);
38
+ $('<td>').append(renderDuration(item['averageViewRuntime'])).appendTo(row);
39
+ $('<td>').append(renderDuration(item['slowestDuration'])).appendTo(row);
40
+ $('<td>').append(renderDuration(item['slowestDbRuntime'])).appendTo(row);
41
+ $('<td>').append(renderDuration(item['slowestViewRuntime'])).appendTo(row);
42
+ // // Action column
43
+ // var actionTd = $('<td>');
44
+ // var actionButton = $('<button>').text('Details');
45
+ // actionButton.on('click', function() {
46
+ // // Replace 'your-details-url' with the actual URL where the details are to be fetched.
47
+ // // It's assumed the ID is required as a URL parameter, modify as per your requirements.
48
+ // window.location.href = '<%= request_details_path %>?request_id=' + item['request_id'];
49
+ // });
50
+ // actionButton.appendTo(actionTd);
51
+ // actionTd.appendTo(row);
52
+
53
+ // Append the row to the table body
54
+ tableBody.append(row);
55
+ });
56
+ }
57
+
58
+ function renderDuration(duration) {
59
+ var durationClass;
60
+ if (duration > 0 && duration < 500) {
61
+ durationClass = "duration-circle green-circle";
62
+ } else if (duration >= 500 && duration < 1000) {
63
+ durationClass = "duration-circle amber-circle";
64
+ } else if (duration >= 1000){
65
+ durationClass = "duration-circle red-circle";
66
+ }
67
+
68
+ var formattedDuration = duration == null || duration == 0 ? 'n/a' : parseFloat(parseFloat(duration).toFixed(3)) + 'ms';
69
+
70
+ // Create the duration span with the circle and the duration value
71
+ return $("<span>").addClass("duration").append(
72
+ $("<span>").addClass(durationClass),
73
+ `${formattedDuration}`
74
+ );
75
+ }
76
+
77
+ </script>
@@ -0,0 +1,204 @@
1
+ <script>
2
+ var request_id = '<%= @request_id %>';
3
+ var current_index = 0;
4
+
5
+ $(document).ready(function() {
6
+ prepPage();
7
+ loadLocalData(function(){
8
+ var request = loadRequest(request_id);
9
+ tagQueriesWithNPlusOne(request);
10
+ const preparedData = flattenData(request.stack);
11
+
12
+ $("#details_div").empty()
13
+ if(request.exception_message) {
14
+ $("#details_div").append(errorDetails(request));
15
+ }
16
+ $("#details_div").append(renderDetails(preparedData));
17
+
18
+ // if(request.has_n_plus_one){
19
+ // // Find all div elements with 'possible_n_plus_one' class
20
+ // $('div.possible_n_plus_one').each(function() {
21
+ // // Traverse up the parent hierarchy
22
+ // $(this).parents().each(function() {
23
+ // // If the parent has the 'has_children' class, trigger a click event
24
+ // $(this).show();
25
+ // });
26
+ // });
27
+ // }
28
+ });
29
+ });
30
+
31
+ function prepPage(){
32
+ $("#details_div").text("Loading Request Now...");
33
+ $("#options").empty();
34
+
35
+ // Create a new checkbox
36
+ var checkbox = $('<input />', {
37
+ type: 'checkbox',
38
+ id: 'chkShowMinorCalls',
39
+ value: 'showMinorCalls'
40
+ }).on('change', function() {
41
+ // Show/hide '.minor_call' elements based on checkbox status
42
+ if(this.checked) {
43
+ $('.minor_call').show();
44
+ } else {
45
+ $('.minor_call').hide();
46
+ }
47
+ });
48
+
49
+ // Create a label for the checkbox
50
+ var label = $('<label />', {
51
+ 'for': 'chkShowMinorCalls',
52
+ text: 'Show Calls with less than 0.1ms duration',
53
+ });
54
+
55
+ // Append the checkbox and label to the div
56
+ $("#options").append(checkbox).append(label);
57
+ }
58
+
59
+ function flattenData(stackData, level = 0) {
60
+ var result = [];
61
+
62
+ stackData.forEach(function(stackItem, level) {
63
+ var start = new Date(stackItem.start_time);
64
+ var end = new Date(stackItem.end_time);
65
+
66
+ if(stackItem.sql) {
67
+ result.push({
68
+ start: start,
69
+ duration: stackItem.duration,
70
+ sql: stackItem.sql,
71
+ cached: stackItem.cached,
72
+ record_count: stackItem.record_count,
73
+ triggering_line: stackItem.triggering_line,
74
+ params: stackItem.params,
75
+ level: level,
76
+ possibleNPlusOne: stackItem.possibleNPlusOne
77
+ });
78
+ } else {
79
+ var children = [];
80
+ if (stackItem.sql_events && stackItem.sql_events.length > 0) {
81
+ children = children.concat(flattenData(stackItem.sql_events, level+1));
82
+ }
83
+
84
+ if (stackItem.children && stackItem.children.length > 0) {
85
+ children = children.concat(flattenData(stackItem.children, level+1));
86
+ }
87
+ result.push({
88
+ start: start,
89
+ duration: stackItem.duration,
90
+ method: stackItem.method,
91
+ method_line: stackItem.method_line,
92
+ triggering_line: stackItem.triggering_line,
93
+ children: children,
94
+ level
95
+ });
96
+ }
97
+ });
98
+
99
+ // sort by start_time
100
+ result.sort((a, b) => a.start - b.start);
101
+
102
+ return result;
103
+ }
104
+ function getDurationClass(duration) {
105
+ if (duration < 500) {
106
+ return "green-circle";
107
+ } else if (duration >= 500 && duration < 1000) {
108
+ return "amber-circle";
109
+ } else {
110
+ return "red-circle";
111
+ }
112
+ }
113
+
114
+ function getFormattedDate(date) {
115
+ return `${date.toLocaleDateString('en-GB')} ${date.toLocaleTimeString('en-GB', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' })}`;
116
+ }
117
+
118
+ function renderDetails(stackData, level = 0) {
119
+ return stackData.map((item, index) => {
120
+ var itemDiv = $("<div>").addClass('bounding-box');
121
+
122
+ var formattedDate = getFormattedDate(item.start);
123
+ var formattedDuration = parseFloat(parseFloat(item.duration).toFixed(3)) + 'ms';
124
+ var durationClass = getDurationClass(item.duration);
125
+
126
+ var durationSpan = $("<span>").addClass("duration").append(
127
+ $("<span>").addClass("duration-circle " + durationClass),
128
+ `${formattedDuration}`
129
+ );
130
+
131
+ if (item.method) { // If it's a method.
132
+ if(item.duration < 0.1 && level > 0){
133
+ itemDiv.addClass('minor_call');
134
+ }
135
+ itemDiv.addClass("method")
136
+ .append($('<span>').addClass('date').text(formattedDate), durationSpan,
137
+ $('<span>').addClass('description').text(item.method),
138
+ $('<span>').addClass('method_line').text(item.method_line),
139
+ $('<span>').addClass('trigger_line').text(item.triggering_line));
140
+
141
+ if(item.children && item.children.length > 0) {
142
+ itemDiv.prepend($("<span>").addClass('has_children').text("+"))
143
+ .append(
144
+ $("<div>").addClass("children")
145
+ .append(renderDetails(item.children, level +1))
146
+ .hide() // Hide the children initially
147
+ );
148
+ }
149
+ } else { // If it's an SQL event.
150
+ itemDiv.addClass("sql-event")
151
+ .append($('<span>').addClass('date').text(formattedDate), durationSpan,
152
+ $('<span>').addClass('record_count').text(item.record_count + ' Results'),
153
+ $('<span>').addClass('cached').text(item.cached ? 'Cached' : ''),
154
+ $('<span>').addClass('n_plus_one').text(item.possibleNPlusOne ? 'Possible N+1' : ''),
155
+ $('<span>').addClass('trigger_line').text(item.triggering_line),
156
+ $('<span>').addClass('sql').text(item.sql));
157
+
158
+ if(item.possibleNPlusOne) {
159
+ itemDiv.addClass('possible_n_plus_one');
160
+ }
161
+ if(item.params && item.params.length > 0) {
162
+ itemDiv.append($('<span>').addClass('params').text('Params: ' + item.params));
163
+ }
164
+ }
165
+ return itemDiv;
166
+ });
167
+ }
168
+
169
+ $(document).on('click', '.bounding-box', function(e) {
170
+ e.stopPropagation();
171
+ var toggleElement = $(this).find(".has_children:first");
172
+ if (toggleElement.length > 0) {
173
+ if (toggleElement.text() === "+") {
174
+ toggleElement.text("-");
175
+ } else {
176
+ toggleElement.text("+");
177
+ }
178
+ $(this).find(".children:first").slideToggle();
179
+ }
180
+ });
181
+
182
+
183
+ function errorDetails(request) {
184
+ // Create a container for the error details
185
+ var errorContainer = $('<div>').addClass('error_status');
186
+
187
+ // Create and append elements for the exception class and message
188
+ var exceptionClassElement = $('<h2>').text(request.exception_class).css('margin', '0');
189
+ var exceptionMessageElement = $('<p>').text(request.exception_message);
190
+ errorContainer.append(exceptionClassElement, exceptionMessageElement);
191
+
192
+ // Create and append an element for the backtrace
193
+ var backtraceElement = $('<ol>');
194
+ request.exception_backtrace.forEach(function(line) {
195
+ var lineElement = $('<li>').text(line);
196
+ backtraceElement.append(lineElement);
197
+ });
198
+ errorContainer.append(backtraceElement);
199
+
200
+ // Append the error details container to the body of the page
201
+ return errorContainer;
202
+ }
203
+
204
+ </script>
@@ -0,0 +1,74 @@
1
+ <script>
2
+ $(document).ready(function() {
3
+ fetchDataAndUpdateStorage(function(updatedData) {
4
+ renderTable(updatedData);
5
+ });
6
+
7
+ setInterval(function() {
8
+ var autoUpdate = $('#autoUpdate').is(':checked');
9
+ if (autoUpdate) {
10
+ fetchDataAndUpdateStorage(function(updatedData) {
11
+ renderTable(updatedData);
12
+ });
13
+ }
14
+ }, 5000);
15
+
16
+ addSortingClick();
17
+ });
18
+
19
+ function renderTable(data, target = null) {
20
+ data = data.filter(item => item.duration > 1500);
21
+ data = sortDataBy(current_sort_field, data);
22
+
23
+ // Get a reference to the table body
24
+ var tableBody = target || $('table tbody');
25
+
26
+ // Clear the table body
27
+ tableBody.empty();
28
+
29
+ // Create new table rows for each data item
30
+ data.forEach(function(item) {
31
+ var row = $('<tr>');
32
+
33
+ $('<td>').text(new Date(item['start_time']).toLocaleString()).appendTo(row);
34
+ $('<td>').text(item['controller'] + '#' + item['action']).appendTo(row);
35
+ $('<td>').text(item['status']).appendTo(row);
36
+ $('<td>').append(renderDuration(item['duration'])).appendTo(row);
37
+ $('<td>').append(renderDuration(item['db_runtime'])).appendTo(row);
38
+ $('<td>').append(renderDuration(item['view_runtime'])).appendTo(row);
39
+ // Action column
40
+ var actionTd = $('<td>');
41
+ var actionButton = $('<button>').text('Details');
42
+ actionButton.on('click', function() {
43
+ // Replace 'your-details-url' with the actual URL where the details are to be fetched.
44
+ // It's assumed the ID is required as a URL parameter, modify as per your requirements.
45
+ window.location.href = '<%= request_details_path %>?request_id=' + item['request_id'];
46
+ });
47
+ actionButton.appendTo(actionTd);
48
+ actionTd.appendTo(row);
49
+
50
+ // Append the row to the table body
51
+ tableBody.append(row);
52
+ });
53
+ }
54
+
55
+ function renderDuration(duration) {
56
+ var durationClass;
57
+ if (duration > 0 && duration < 500) {
58
+ durationClass = "duration-circle green-circle";
59
+ } else if (duration >= 500 && duration < 1000) {
60
+ durationClass = "duration-circle amber-circle";
61
+ } else if (duration >= 1000){
62
+ durationClass = "duration-circle red-circle";
63
+ }
64
+
65
+ var formattedDuration = duration == null || duration == 0 ? 'n/a' : parseFloat(parseFloat(duration).toFixed(3)) + 'ms';
66
+
67
+ // Create the duration span with the circle and the duration value
68
+ return $("<span>").addClass("duration").append(
69
+ $("<span>").addClass(durationClass),
70
+ `${formattedDuration}`
71
+ );
72
+ }
73
+
74
+ </script>
@@ -0,0 +1,21 @@
1
+ <%= render 'builder_apm/shared/header' %>
2
+
3
+ <table>
4
+ <thead>
5
+ <tr>
6
+ <th class="sortable" data-field="start_time">Time</th>
7
+ <th class="sortable" data-field="controller">Controller#Action</th>
8
+ <th class="sortable" data-field="status">Status</th>
9
+ <th class="sortable" data-field="real_duration_time">Duration (ms)</th>
10
+ <th class="sortable" data-field="calc_db_runtime">DB Runtime (ms)</th>
11
+ <th class="sortable" data-field="view_runtime">View Runtime (ms)</th>
12
+ </tr>
13
+ </thead>
14
+ <tbody>
15
+ <!-- Table content will be populated here by JavaScript -->
16
+ </tbody>
17
+ </table>
18
+
19
+ <%= render 'builder_apm/js/n_plus_one' %>
20
+
21
+ <%= render 'builder_apm/shared/footer' %>
@@ -0,0 +1,21 @@
1
+ <%= render 'builder_apm/shared/header' %>
2
+
3
+ <table>
4
+ <thead>
5
+ <tr>
6
+ <th class="sortable" data-field="start_time">Time</th>
7
+ <th class="sortable" data-field="controller">Controller#Action</th>
8
+ <th class="sortable" data-field="status">Status</th>
9
+ <th class="sortable" data-field="real_duration_time">Duration (ms)</th>
10
+ <th class="sortable" data-field="calc_db_runtime">DB Runtime (ms)</th>
11
+ <th class="sortable" data-field="view_runtime">View Runtime (ms)</th>
12
+ </tr>
13
+ </thead>
14
+ <tbody>
15
+ <!-- Table content will be populated here by JavaScript -->
16
+ </tbody>
17
+ </table>
18
+
19
+ <%= render 'builder_apm/js/recent_requests' %>
20
+
21
+ <%= render 'builder_apm/shared/footer' %>
@@ -0,0 +1,24 @@
1
+ <%= render 'builder_apm/shared/header' %>
2
+
3
+ <table>
4
+ <thead>
5
+ <tr>
6
+ <th class="sortable" data-field="controller">Controller#Action</th>
7
+ <th class="sortable" data-field="path">Url</th>
8
+ <th class="sortable" data-field="count">Requests</th>
9
+ <th class="sortable" data-field="averageDuration">Avg Duration (ms)</th>
10
+ <th class="sortable" data-field="averageDbRuntime">Avg DB Runtime (ms)</th>
11
+ <th class="sortable" data-field="averageViewRuntime">Avg View Runtime (ms)</th>
12
+ <th class="sortable" data-field="slowestDuration">Slowest Duration (ms)</th>
13
+ <th class="sortable" data-field="slowestDbRuntime">Slowest DB Runtime (ms)</th>
14
+ <th class="sortable" data-field="slowestViewRuntime">Slowest View Runtime (ms)</th>
15
+ </tr>
16
+ </thead>
17
+ <tbody>
18
+ <!-- Table content will be populated here by JavaScript -->
19
+ </tbody>
20
+ </table>
21
+
22
+ <%= render 'builder_apm/js/request_analysis' %>
23
+
24
+ <%= render 'builder_apm/shared/footer' %>
@@ -0,0 +1,7 @@
1
+ <%= render 'builder_apm/shared/header' %>
2
+
3
+ <div id="details_div"></div>
4
+
5
+ <%= render 'builder_apm/js/request_details' %>
6
+
7
+ <%= render 'builder_apm/shared/footer' %>
@@ -0,0 +1,3 @@
1
+
2
+ </body>
3
+ </html>
@@ -0,0 +1,55 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>BuilderApm Dashboard</title>
5
+ <%= render 'builder_apm/css/main' %>
6
+ <%= render 'builder_apm/css/dark' %>
7
+ <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
8
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/dygraph/2.1.0/dygraph.min.js"></script>
9
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/dygraph/2.1.0/dygraph.min.css" />
10
+ <%= render 'builder_apm/js/data_fetcher' %>
11
+ <script>
12
+ $(document).ready(function() {
13
+
14
+ // Check for saved 'darkMode' in localStorage
15
+ if (localStorage.getItem('darkMode') === 'true') {
16
+ $('body').addClass('dark');
17
+ }
18
+
19
+ $('#clearData').on('click', function() {
20
+ localStorage.removeItem('builder_apm_requests');
21
+ localStorage.removeItem('builder_apm_cursor');
22
+ alert('Local Data Cleared');
23
+ });
24
+ $('#darkModeToggle').on('click', function() {
25
+ $('body').toggleClass('dark');
26
+
27
+ // Save the current mode in localStorage
28
+ if ($('body').hasClass('dark')) {
29
+ localStorage.setItem('darkMode', 'true');
30
+ } else {
31
+ localStorage.setItem('darkMode', 'false');
32
+ }
33
+ });
34
+ $('body').show();
35
+ });
36
+ </script>
37
+ </head>
38
+ <body>
39
+ <h1 id="header">BuilderApm Dashboard</h1>
40
+ <nav id="navbar">
41
+ <ul>
42
+ <li><%= link_to 'Dashboard', dashboard_path, class: ("active" if current_page?(dashboard_path)) %></li>
43
+ <li><%= link_to 'Request Analysis', request_analysis_path, class: ("active" if current_page?(request_analysis_path)) %></li>
44
+ <li><%= link_to '500 Errors', errors_500_path, class: ("active" if current_page?(errors_500_path)) %></li>
45
+ <li><%= link_to 'Recent Requests', recent_requests_path, class: ("active" if current_page?(recent_requests_path)) %></li>
46
+ <li><%= link_to 'Slow Requests', slow_requests_path, class: ("active" if current_page?(slow_requests_path)) %></li>
47
+ <li><%= link_to 'N+1', n_plus_one_path, class: ("active" if current_page?(n_plus_one_path)) %></li>
48
+ <li id="dark-mode-toggle"><button id="darkModeToggle" class="nav-button">Toggle Dark Mode</button></li>
49
+ <li id="dark-mode-toggle"><button id="clearData" class="nav-button">Clear Data</button></li>
50
+ </ul>
51
+ </nav>
52
+ <div id="options">
53
+ <input type="checkbox" id="autoUpdate" name="autoUpdate">
54
+ <label for="autoUpdate">Auto-Update</label>
55
+ </div>