builder_apm 0.4.2 → 0.5.1
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 +4 -4
- data/Gemfile.lock +1 -1
- data/app/controllers/builder_apm/diagnose_request_controller.rb +79 -0
- data/app/controllers/builder_apm/request_details_controller.rb +3 -2
- data/app/views/builder_apm/css/_dark.html.erb +161 -118
- data/app/views/builder_apm/css/_main.html.erb +342 -272
- data/app/views/builder_apm/js/_data_fetcher.html.erb +229 -236
- data/app/views/builder_apm/js/_request_details.html.erb +368 -164
- data/builder_apm.gemspec +1 -1
- data/config/routes.rb +3 -0
- data/lib/builder_apm/configuration.rb +6 -0
- data/lib/builder_apm/doctor/ai_doctor.rb +170 -0
- data/lib/builder_apm/doctor/backtrace_reducer.rb +104 -0
- data/lib/builder_apm/doctor/bravo_chat_ai.rb +85 -0
- data/lib/builder_apm/doctor/openai_chat_gpt.rb +84 -0
- data/lib/builder_apm/methods/instrumenter.rb +33 -6
- data/lib/builder_apm/middleware/timing.rb +6 -0
- data/lib/builder_apm/models/instrumenter.rb +15 -3
- data/lib/builder_apm/version.rb +1 -1
- data/lib/generators/builder_apm/templates/builder_apm_config.rb +7 -4
- metadata +8 -4
- data/README.md +0 -29
@@ -1,192 +1,396 @@
|
|
1
1
|
<script>
|
2
|
-
var request_id = '<%= @request_id %>';
|
3
|
-
var
|
4
|
-
var
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
var
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
}).on('change', function() {
|
29
|
-
// Show/hide '.minor_call' elements based on checkbox status
|
30
|
-
if(this.checked) {
|
31
|
-
$('.minor_call').show();
|
32
|
-
} else {
|
33
|
-
$('.minor_call').hide();
|
2
|
+
var request_id = '<%= @request_id %>';
|
3
|
+
var request = JSON.parse(<%= @data.to_json.html_safe %>);
|
4
|
+
var diagnosis = <%= @diagnosis.to_json.html_safe %>;
|
5
|
+
var deeper_diagnosis = <%= @deeper_diagnosis.to_json.html_safe %>;
|
6
|
+
var has_api_key = <%= @has_api_key.to_s %>;
|
7
|
+
|
8
|
+
var current_index = 0;
|
9
|
+
|
10
|
+
$(document).ready(function () {
|
11
|
+
prepPage();
|
12
|
+
tagQueriesWithNPlusOne(request);
|
13
|
+
const preparedData = flattenData(request.stack);
|
14
|
+
|
15
|
+
mainDiv = $("#details_div")
|
16
|
+
|
17
|
+
mainDiv.empty();
|
18
|
+
mainDiv.append(renderRequestSummary(request));
|
19
|
+
if (diagnosis.length > 0) {
|
20
|
+
mainDiv.append(renderRequestDiagnosis(diagnosis));
|
21
|
+
}
|
22
|
+
if (deeper_diagnosis.length > 0) {
|
23
|
+
mainDiv.append(renderDeeperDiagnosis(deeper_diagnosis));
|
24
|
+
}
|
25
|
+
|
26
|
+
if (request.exception_message) {
|
27
|
+
mainDiv.append(errorDetails(request));
|
34
28
|
}
|
35
|
-
});
|
36
29
|
|
37
|
-
|
38
|
-
var label = $('<label />', {
|
39
|
-
'for': 'chkShowMinorCalls',
|
40
|
-
text: 'Show Calls with less than 0.1ms duration',
|
30
|
+
mainDiv.append(renderDetails(preparedData));
|
41
31
|
});
|
42
32
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
duration: stackItem.duration,
|
58
|
-
sql: stackItem.sql,
|
59
|
-
cached: stackItem.cached,
|
60
|
-
record_count: stackItem.record_count,
|
61
|
-
triggering_line: stackItem.triggering_line,
|
62
|
-
params: stackItem.params,
|
63
|
-
level: level,
|
64
|
-
possibleNPlusOne: stackItem.possibleNPlusOne
|
65
|
-
});
|
66
|
-
} else {
|
67
|
-
var children = [];
|
68
|
-
if (stackItem.sql_events && stackItem.sql_events.length > 0) {
|
69
|
-
children = children.concat(flattenData(stackItem.sql_events, level+1));
|
33
|
+
function prepPage() {
|
34
|
+
$("#details_div").text("Loading Request Now...");
|
35
|
+
$("#options").empty();
|
36
|
+
|
37
|
+
// Create a new checkbox
|
38
|
+
var checkbox = $('<input />', {
|
39
|
+
type: 'checkbox',
|
40
|
+
id: 'chkShowMinorCalls',
|
41
|
+
value: 'showMinorCalls'
|
42
|
+
}).on('change', function () { // Show/hide '.minor_call' elements based on checkbox status
|
43
|
+
if (this.checked) {
|
44
|
+
$('.minor_call').show();
|
45
|
+
} else {
|
46
|
+
$('.minor_call').hide();
|
70
47
|
}
|
48
|
+
});
|
49
|
+
|
50
|
+
// Create a label for the checkbox
|
51
|
+
var label = $('<label />', {
|
52
|
+
'for': 'chkShowMinorCalls',
|
53
|
+
text: 'Show Calls with less than 0.1ms duration'
|
54
|
+
});
|
55
|
+
|
56
|
+
// Append the checkbox and label to the div
|
57
|
+
$("#options").append(checkbox).append(label);
|
58
|
+
}
|
59
|
+
|
60
|
+
function callTheDoctor() {
|
61
|
+
$('.ai-doctor-button').hide();
|
62
|
+
|
63
|
+
// Show loading message with a simple animated "..." indicator
|
64
|
+
var loaded = false;
|
65
|
+
var dots = 0;
|
66
|
+
var doc_msg = getRandomMessage(wait_4_doc);
|
67
|
+
var loadingInterval = setInterval(function () {
|
68
|
+
$('.status-message').text(doc_msg + '.'.repeat(dots));
|
69
|
+
dots = (dots + 1) % 4;
|
70
|
+
}, 500);
|
71
|
+
|
72
|
+
function fetchDiagnosis() {
|
73
|
+
return $.get('/builder_apm/diagnose_request?request_id=' + request_id);
|
74
|
+
}
|
71
75
|
|
72
|
-
|
73
|
-
|
76
|
+
|
77
|
+
Promise.race([fetchDiagnosis()]).then(response => {
|
78
|
+
if (response && response.full_diagnosis) {
|
79
|
+
clearInterval(loadingInterval);
|
80
|
+
renderRequestDiagnosis(response.full_diagnosis).insertAfter('.request-summary');
|
81
|
+
$('.status-message').hide();
|
82
|
+
return;
|
83
|
+
} else {
|
84
|
+
throw new Error("Failed to get valid response.");
|
74
85
|
}
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
86
|
+
}).catch(err => {
|
87
|
+
console.error("Error:", err);
|
88
|
+
clearInterval(loadingInterval);
|
89
|
+
$('.ai-doctor-button').show();
|
90
|
+
$('.status-message').text(getRandomMessage(doc_not_in));
|
91
|
+
});
|
92
|
+
}
|
93
|
+
|
94
|
+
|
95
|
+
function getRandomMessage(messages) {
|
96
|
+
let randomIndex = Math.floor(Math.random() * messages.length);
|
97
|
+
return messages[randomIndex];
|
98
|
+
}
|
99
|
+
|
100
|
+
var wait_4_doc = [
|
101
|
+
"The Ai Doctor is on it! Analyzing your data now; please wait a moment.",
|
102
|
+
"Hold tight! The Ai Doctor is checking your stack trace and data.",
|
103
|
+
"Your request is being analyzed by the Ai Doctor. Give it a few minutes.",
|
104
|
+
"Summoning the Ai Doctor to review your input. Sit tight!",
|
105
|
+
"Please be patient. The Ai Doctor is examining your data for any anomalies.",
|
106
|
+
"Analysis in progress! The Ai Doctor is diving deep into your data.",
|
107
|
+
"The Ai Doctor is now on duty! Reviewing the provided details.",
|
108
|
+
"Your data is on the examination table! The Ai Doctor will be with you shortly.",
|
109
|
+
"Sending your information to the Ai Doctor for a thorough check-up. Hang on!",
|
110
|
+
"Ai Doctor is rolling up its sleeves! Your data is being analyzed, please wait.",
|
111
|
+
"The doctor is being called and will review your notes. This may take a couple of mins."
|
112
|
+
];
|
113
|
+
|
114
|
+
var doc_not_in = [
|
115
|
+
"Oops! The Ai Doctor seems to be offline. Please retry in a bit.",
|
116
|
+
"Sorry, the Ai Doctor is currently out of reach. Give it another shot later.",
|
117
|
+
"Our apologies! The Ai Doctor is taking a short break. Try again shortly.",
|
118
|
+
"It looks like the Ai Doctor is tied up. Please attempt again later.",
|
119
|
+
"Regrettably, the Ai Doctor can't be reached right now. Please come back later.",
|
120
|
+
"Technical hiccup! The Ai Doctor isn't responsive. Kindly retry after some time.",
|
121
|
+
"Brief interruption — the Ai Doctor is momentarily out. Please revisit in a while.",
|
122
|
+
"We're experiencing a glitch. The Ai Doctor isn't on call right now. Please retry later.",
|
123
|
+
"Ai Doctor seems to be in a meeting. Please push the button again later.",
|
124
|
+
"Hold up! The Ai Doctor isn't taking appointments right now. Try after a moment.",
|
125
|
+
"The doctor is unavailable, please try again later"
|
126
|
+
];
|
127
|
+
|
128
|
+
var no_key_4_doc = [
|
129
|
+
"Ai Doctor is currently inaccessible - API Key hasn't been configured.",
|
130
|
+
"Unable to access Ai Doctor - Missing API Key.",
|
131
|
+
"Ai Doctor can't be reached - Please set up your API Key.",
|
132
|
+
"Error: Ai Doctor is unavailable due to an unset API Key.",
|
133
|
+
"Ai Doctor service is offline - No API Key detected.",
|
134
|
+
"Ai Doctor cannot proceed - API Key configuration required.",
|
135
|
+
"Operation halted: Ai Doctor requires an API Key setup.",
|
136
|
+
"Api Key for Ai Doctor is missing - Service not accessible.",
|
137
|
+
"Ai Doctor is not operational - Ensure your API Key is established.",
|
138
|
+
"Connectivity issue with Ai Doctor - API Key setup needed."
|
139
|
+
];
|
140
|
+
|
141
|
+
function renderRequestDiagnosis(diagnosis) { // Create the main diagnosis div
|
142
|
+
var diagnosisDiv = $('<div>').addClass('diagnosis-section').html('<h2 class="doc_title">Ai Doctor Analysis</h2><p>' + diagnosis + '</p>');
|
143
|
+
|
144
|
+
// Extract the file and line number from the diagnosis string
|
145
|
+
var parser = new DOMParser();
|
146
|
+
var doc = parser.parseFromString(diagnosis, 'text/html');
|
147
|
+
|
148
|
+
if (deeper_diagnosis.length === 0) {
|
149
|
+
// Add the AiDoctor button
|
150
|
+
// Create the deeper analysis button
|
151
|
+
var deeperAnalysisButton = $("<button>").addClass('deeper-analysis-button').html('<i class="cool-icon"></i> Deeper Analysis');
|
152
|
+
var message_div = $("<div>").addClass('deeper-analaysis-status-message');
|
153
|
+
if (has_api_key) {
|
154
|
+
deeperAnalysisButton.on('click', function () {
|
155
|
+
deeperAnalysisHandler();
|
156
|
+
});
|
157
|
+
} else {
|
158
|
+
message_div.text(getRandomMessage(no_key_4_doc));
|
159
|
+
deeperAnalysisButton.addClass('disabled');
|
160
|
+
} diagnosisDiv.append(deeperAnalysisButton);
|
161
|
+
diagnosisDiv.append(message_div)
|
162
|
+
} else {
|
163
|
+
diagnosisDiv.find('input[type="checkbox"]').remove();
|
84
164
|
}
|
85
|
-
});
|
86
165
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
return "green-circle";
|
95
|
-
} else if (duration >= 500 && duration < 1000) {
|
96
|
-
return "amber-circle";
|
97
|
-
} else {
|
98
|
-
return "red-circle";
|
166
|
+
return diagnosisDiv;
|
167
|
+
}
|
168
|
+
|
169
|
+
function renderDeeperDiagnosis(diagnosis) { // Create the main diagnosis div
|
170
|
+
var diagnosisDiv = $('<div>').addClass('diagnosis-section').html('<h2 class="doc_title">Ai Doctor Deeper Analysis</h2><p>' + diagnosis + '</p>');
|
171
|
+
|
172
|
+
return diagnosisDiv;
|
99
173
|
}
|
100
|
-
|
174
|
+
// An example handler function for the deeper analysis button click
|
175
|
+
function deeperAnalysisHandler() {
|
101
176
|
|
102
|
-
|
103
|
-
return `${date.toLocaleDateString('en-GB')} ${date.toLocaleTimeString('en-GB', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' })}`;
|
104
|
-
}
|
177
|
+
var selectedCheckboxes = document.querySelectorAll('input[name="deeper_dive_filename"]:checked');
|
105
178
|
|
106
|
-
|
107
|
-
return stackData.map((item, index) => {
|
108
|
-
var itemDiv = $("<div>").addClass('bounding-box');
|
179
|
+
var selectedFiles = Array.from(selectedCheckboxes).map(checkbox => checkbox.value);
|
109
180
|
|
110
|
-
|
111
|
-
var formattedDuration = parseFloat(parseFloat(item.duration).toFixed(3)) + 'ms';
|
112
|
-
var durationClass = getDurationClass(item.duration);
|
181
|
+
console.log("Deeper analysis for:", selectedFiles);
|
113
182
|
|
114
|
-
|
115
|
-
$("<span>").addClass("duration-circle " + durationClass),
|
116
|
-
`${formattedDuration}`
|
117
|
-
);
|
183
|
+
$('.deeper-analysis-button').hide();
|
118
184
|
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
$('<span>').addClass('sql').text(item.sql));
|
145
|
-
|
146
|
-
if(item.possibleNPlusOne) {
|
147
|
-
itemDiv.addClass('possible_n_plus_one');
|
148
|
-
}
|
149
|
-
if(item.params && item.params.length > 0) {
|
150
|
-
itemDiv.append($('<span>').addClass('params').text('Params: ' + item.params));
|
185
|
+
// Show loading message with a simple animated "..." indicator
|
186
|
+
var loaded = false;
|
187
|
+
var dots = 0;
|
188
|
+
var doc_msg = getRandomMessage(wait_4_doc);
|
189
|
+
var loadingInterval = setInterval(function () {
|
190
|
+
$('.deeper-analaysis-status-message').text(doc_msg + '.'.repeat(dots));
|
191
|
+
dots = (dots + 1) % 4;
|
192
|
+
}, 500);
|
193
|
+
|
194
|
+
function fetchDeeperDiagnosis() {
|
195
|
+
return $.post('/builder_apm/deeper_analysis', {
|
196
|
+
request_id: request_id,
|
197
|
+
files: selectedFiles
|
198
|
+
});
|
199
|
+
}
|
200
|
+
|
201
|
+
|
202
|
+
Promise.race([fetchDeeperDiagnosis()]).then(response => {
|
203
|
+
if (response && response.deeper_diagnosis) {
|
204
|
+
clearInterval(loadingInterval);
|
205
|
+
renderDeeperDiagnosis(response.deeper_diagnosis).insertAfter('.diagnosis-section');
|
206
|
+
$('.deeper-analaysis-status-message').hide();
|
207
|
+
return;
|
208
|
+
} else {
|
209
|
+
throw new Error("Failed to get valid response.");
|
151
210
|
}
|
211
|
+
}).catch(err => {
|
212
|
+
console.error("Error:", err);
|
213
|
+
clearInterval(loadingInterval);
|
214
|
+
$('.ai-doctor-button').show();
|
215
|
+
$('.deeper-analaysis-status-message').text(getRandomMessage(doc_not_in));
|
216
|
+
});
|
217
|
+
}
|
218
|
+
|
219
|
+
|
220
|
+
function renderRequestSummary(request) {
|
221
|
+
var summaryDiv = $("<div>").addClass('request-summary');
|
222
|
+
|
223
|
+
// Add the details to the summary
|
224
|
+
summaryDiv.append($('<span>').addClass('url').text('URL: ' + request.path));
|
225
|
+
summaryDiv.append($('<span>').addClass('controller').text('Controller: ' + request.controller));
|
226
|
+
summaryDiv.append($('<span>').addClass('duration').text('Duration: ' + formatDuration(request.duration) + 'ms'));
|
227
|
+
summaryDiv.append($('<span>').addClass('db_duration').text('DB Duration: ' + formatDuration(request.db_runtime) + 'ms'));
|
228
|
+
summaryDiv.append($('<span>').addClass('view_duration').text('View Duration: ' + formatDuration(request.view_runtime) + 'ms'));
|
229
|
+
summaryDiv.append($('<span>').addClass('method').text('Method: ' + request.method));
|
230
|
+
summaryDiv.append($('<span>').addClass('params').text('Params: ' + JSON.stringify(request.params)));
|
231
|
+
|
232
|
+
if (diagnosis.length === 0) { // Add the AiDoctor button
|
233
|
+
var aiDoctorButton = $("<button>").addClass('ai-doctor-button').html('<i class="cool-icon"></i> Call AiDoctor');
|
234
|
+
var message_div = $("<div>").addClass('status-message');
|
235
|
+
if (has_api_key) {
|
236
|
+
aiDoctorButton.on('click', callTheDoctor);
|
237
|
+
} else {
|
238
|
+
message_div.text(getRandomMessage(no_key_4_doc));
|
239
|
+
aiDoctorButton.addClass('disabled');
|
240
|
+
} summaryDiv.append(aiDoctorButton);
|
241
|
+
summaryDiv.append(message_div)
|
152
242
|
}
|
153
|
-
return
|
154
|
-
}
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
var toggleElement = $(this).find(".has_children:first");
|
160
|
-
if (toggleElement.length > 0) {
|
161
|
-
if (toggleElement.text() === "+") {
|
162
|
-
toggleElement.text("-");
|
243
|
+
return summaryDiv;
|
244
|
+
}
|
245
|
+
|
246
|
+
function formatDuration(value) {
|
247
|
+
if (isNaN(value)) {
|
248
|
+
return 'N/A'; // or whatever default/failover value you prefer
|
163
249
|
} else {
|
164
|
-
|
250
|
+
return parseFloat(value).toFixed(3);
|
165
251
|
}
|
166
|
-
$(this).find(".children:first").slideToggle();
|
167
252
|
}
|
168
|
-
});
|
169
253
|
|
254
|
+
function flattenData(stackData, level = 0) {
|
255
|
+
var result = [];
|
256
|
+
|
257
|
+
stackData.forEach(function (stackItem, level) {
|
258
|
+
var start = new Date(stackItem.start_time);
|
259
|
+
var end = new Date(stackItem.end_time);
|
260
|
+
|
261
|
+
if (stackItem.sql) {
|
262
|
+
result.push({
|
263
|
+
start: start,
|
264
|
+
duration: stackItem.duration,
|
265
|
+
sql: stackItem.sql,
|
266
|
+
cached: stackItem.cached,
|
267
|
+
record_count: stackItem.record_count,
|
268
|
+
triggering_line: stackItem.triggering_line,
|
269
|
+
params: stackItem.params,
|
270
|
+
level: level,
|
271
|
+
possibleNPlusOne: stackItem.possibleNPlusOne
|
272
|
+
});
|
273
|
+
} else {
|
274
|
+
var children = [];
|
275
|
+
if (stackItem.sql_events && stackItem.sql_events.length > 0) {
|
276
|
+
children = children.concat(flattenData(stackItem.sql_events, level + 1));
|
277
|
+
}
|
170
278
|
|
171
|
-
|
172
|
-
|
173
|
-
|
279
|
+
if (stackItem.children && stackItem.children.length > 0) {
|
280
|
+
children = children.concat(flattenData(stackItem.children, level + 1));
|
281
|
+
}
|
282
|
+
result.push({
|
283
|
+
start: start,
|
284
|
+
duration: stackItem.duration,
|
285
|
+
method: stackItem.method,
|
286
|
+
method_line: stackItem.method_line,
|
287
|
+
triggering_line: stackItem.triggering_line,
|
288
|
+
children: children,
|
289
|
+
level
|
290
|
+
});
|
291
|
+
}
|
292
|
+
});
|
174
293
|
|
175
|
-
|
176
|
-
|
177
|
-
var exceptionMessageElement = $('<p>').text(request.exception_message);
|
178
|
-
errorContainer.append(exceptionClassElement, exceptionMessageElement);
|
294
|
+
// sort by start_time
|
295
|
+
result.sort((a, b) => a.start - b.start);
|
179
296
|
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
297
|
+
return result;
|
298
|
+
}
|
299
|
+
function getDurationClass(duration) {
|
300
|
+
if (duration < 500) {
|
301
|
+
return "green-circle";
|
302
|
+
} else if (duration >= 500 && duration < 1000) {
|
303
|
+
return "amber-circle";
|
304
|
+
} else {
|
305
|
+
return "red-circle";
|
306
|
+
}
|
307
|
+
}
|
308
|
+
|
309
|
+
function getFormattedDate(date) {
|
310
|
+
return `${
|
311
|
+
date.toLocaleDateString('en-GB')
|
312
|
+
} ${
|
313
|
+
date.toLocaleTimeString('en-GB', {
|
314
|
+
hour12: false,
|
315
|
+
hour: '2-digit',
|
316
|
+
minute: '2-digit',
|
317
|
+
second: '2-digit'
|
318
|
+
})
|
319
|
+
}`;
|
320
|
+
}
|
321
|
+
|
322
|
+
function renderDetails(stackData, level = 0) {
|
323
|
+
return stackData.map((item, index) => {
|
324
|
+
var itemDiv = $("<div>").addClass('bounding-box');
|
325
|
+
|
326
|
+
var formattedDate = getFormattedDate(item.start);
|
327
|
+
var formattedDuration = parseFloat(parseFloat(item.duration).toFixed(3)) + 'ms';
|
328
|
+
var durationClass = getDurationClass(item.duration);
|
329
|
+
|
330
|
+
var durationSpan = $("<span>").addClass("duration").append($("<span>").addClass("duration-circle " + durationClass), `${formattedDuration}`);
|
331
|
+
|
332
|
+
if (item.method) { // If it's a method.
|
333
|
+
if (item.duration < 0.1 && level > 0) {
|
334
|
+
itemDiv.addClass('minor_call');
|
335
|
+
}
|
336
|
+
itemDiv.addClass("method").append($('<span>').addClass('date').text(formattedDate), durationSpan, $('<span>').addClass('description').text(item.method), $('<span>').addClass('method_line').text(item.method_line), $('<span>').addClass('trigger_line').text(item.triggering_line));
|
337
|
+
|
338
|
+
if (item.children && item.children.length > 0) {
|
339
|
+
itemDiv.prepend($("<span>").addClass('has_children').text("+")).append($("<div>").addClass("children").append(renderDetails(item.children, level + 1)).hide() // Hide the children initially
|
340
|
+
);
|
341
|
+
}
|
342
|
+
} else { // If it's an SQL event.
|
343
|
+
let resultsSpan = $('<span>').addClass('record_count').text(item.record_count + ' Results');
|
344
|
+
|
345
|
+
// Check if results are more than 50
|
346
|
+
if (item.record_count > 50) {
|
347
|
+
resultsSpan.addClass('highlight-results').attr('title', "This is a large number of results. Review if this can be paginated or limited.");
|
348
|
+
}
|
349
|
+
|
350
|
+
itemDiv.addClass("sql-event").append($('<span>').addClass('date').text(formattedDate), durationSpan, resultsSpan, $('<span>').addClass('cached').text(item.cached ? 'Cached' : ''), $('<span>').addClass('n_plus_one').text(item.possibleNPlusOne ? 'Possible N+1' : ''), $('<span>').addClass('trigger_line').text(item.triggering_line), $('<span>').addClass('sql').text(item.sql));
|
351
|
+
|
352
|
+
if (item.possibleNPlusOne) {
|
353
|
+
itemDiv.addClass('possible_n_plus_one');
|
354
|
+
}
|
355
|
+
if (item.params && item.params.length > 0) {
|
356
|
+
itemDiv.append($('<span>').addClass('params').text('Params: ' + item.params));
|
357
|
+
}
|
358
|
+
}
|
359
|
+
return itemDiv;
|
360
|
+
});
|
361
|
+
}
|
362
|
+
|
363
|
+
$(document).on('click', '.bounding-box', function (e) {
|
364
|
+
e.stopPropagation();
|
365
|
+
var toggleElement = $(this).find(".has_children:first");
|
366
|
+
if (toggleElement.length > 0) {
|
367
|
+
if (toggleElement.text() === "+") {
|
368
|
+
toggleElement.text("-");
|
369
|
+
} else {
|
370
|
+
toggleElement.text("+");
|
371
|
+
}
|
372
|
+
$(this).find(".children:first").slideToggle();
|
373
|
+
}
|
185
374
|
});
|
186
|
-
errorContainer.append(backtraceElement);
|
187
375
|
|
188
|
-
// Append the error details container to the body of the page
|
189
|
-
return errorContainer;
|
190
|
-
}
|
191
376
|
|
192
|
-
|
377
|
+
function errorDetails(request) { // Create a container for the error details
|
378
|
+
var errorContainer = $('<div>').addClass('error_status');
|
379
|
+
|
380
|
+
// Create and append elements for the exception class and message
|
381
|
+
var exceptionClassElement = $('<h2>').text(request.exception_class).css('margin', '0');
|
382
|
+
var exceptionMessageElement = $('<p>').text(request.exception_message);
|
383
|
+
errorContainer.append(exceptionClassElement, exceptionMessageElement);
|
384
|
+
|
385
|
+
// Create and append an element for the backtrace
|
386
|
+
var backtraceElement = $('<ol>');
|
387
|
+
request.exception_backtrace.forEach(function (line) {
|
388
|
+
var lineElement = $('<li>').text(line);
|
389
|
+
backtraceElement.append(lineElement);
|
390
|
+
});
|
391
|
+
errorContainer.append(backtraceElement);
|
392
|
+
|
393
|
+
// Append the error details container to the body of the page
|
394
|
+
return errorContainer;
|
395
|
+
}
|
396
|
+
</script>
|
data/builder_apm.gemspec
CHANGED
@@ -15,7 +15,7 @@ Gem::Specification.new do |spec|
|
|
15
15
|
spec.add_dependency "redis", "~> 4.5"
|
16
16
|
|
17
17
|
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
18
|
-
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
18
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) || f == 'README.md' }
|
19
19
|
end
|
20
20
|
spec.bindir = "exe"
|
21
21
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
data/config/routes.rb
CHANGED
@@ -9,4 +9,7 @@ BuilderApm::Engine.routes.draw do
|
|
9
9
|
get 'request_analysis', to: 'request_analysis#index'
|
10
10
|
get 'n_plus_one', to: 'n_plus_one#index'
|
11
11
|
|
12
|
+
post 'deeper_analysis', to: 'diagnose_request#deeper_analysis', defaults: { format: 'json' }
|
13
|
+
get 'diagnose_request', to: 'diagnose_request#index', defaults: { format: 'json' }
|
14
|
+
|
12
15
|
end
|
@@ -5,6 +5,9 @@ module BuilderApm
|
|
5
5
|
attr_accessor :enable_active_record_profiler
|
6
6
|
attr_accessor :enable_methods_profiler
|
7
7
|
attr_accessor :enable_n_plus_one_profiler
|
8
|
+
attr_accessor :api_key
|
9
|
+
attr_accessor :api
|
10
|
+
attr_accessor :gems_to_track
|
8
11
|
|
9
12
|
def initialize
|
10
13
|
@redis_url = 'redis://localhost:6379/0'
|
@@ -12,6 +15,9 @@ module BuilderApm
|
|
12
15
|
@enable_active_record_profiler = true
|
13
16
|
@enable_methods_profiler = true
|
14
17
|
@enable_n_plus_one_profiler = true
|
18
|
+
@api_key = nil
|
19
|
+
@api = 'Bravo'
|
20
|
+
@gems_to_track = ["bx_block_"]
|
15
21
|
end
|
16
22
|
end
|
17
23
|
end
|