builder_apm 0.2.5 → 0.3.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 +4 -4
- data/Gemfile.lock +1 -1
- data/app/controllers/builder_apm/application_controller.rb +7 -0
- data/app/controllers/builder_apm/dashboard_controller.rb +20 -4
- data/app/controllers/builder_apm/error_requests_controller.rb +1 -1
- data/app/controllers/builder_apm/n_plus_one_controller.rb +1 -1
- data/app/controllers/builder_apm/recent_requests_controller.rb +1 -1
- data/app/controllers/builder_apm/request_analysis_controller.rb +1 -1
- data/app/controllers/builder_apm/request_data_controller.rb +36 -35
- data/app/controllers/builder_apm/request_details_controller.rb +4 -1
- data/app/controllers/builder_apm/slow_requests_controller.rb +1 -1
- data/app/controllers/builder_apm/wip_controller.rb +1 -1
- data/app/views/builder_apm/css/_main.html.erb +4 -1
- data/app/views/builder_apm/js/_dashboard.html.erb +4 -6
- data/app/views/builder_apm/js/_data_fetcher.html.erb +58 -37
- data/app/views/builder_apm/js/_error_requests.html.erb +21 -13
- data/app/views/builder_apm/js/_n_plus_one.html.erb +18 -10
- data/app/views/builder_apm/js/_recent_requests.html.erb +22 -18
- data/app/views/builder_apm/js/_request_analysis.html.erb +19 -10
- data/app/views/builder_apm/js/_request_details.html.erb +10 -22
- data/app/views/builder_apm/js/_slow_requests.html.erb +19 -10
- data/app/views/builder_apm/shared/_header.html.erb +0 -1
- data/lib/builder_apm/configuration.rb +2 -0
- data/lib/builder_apm/middleware/timing.rb +21 -4
- data/lib/builder_apm/models/instrumenter.rb +43 -8
- data/lib/builder_apm/version.rb +1 -1
- metadata +2 -1
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 31a9a4393e090f4980960c61d6f5cb2cf307a352534b9f8f264c2238dcd281f4
         | 
| 4 | 
            +
              data.tar.gz: a01935162ea2b1fa2701a080536aadc6259c3a9c102148ef1df4475ddd7a89de
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 61269a7ae1127eebfbdec316e015e64e6e0a76b23d81433c1a2569f4ccfabbebccc289d4f5588dfa46ac100d33dd5269fdc6e37208167598cdee7e6066ec35d5
         | 
| 7 | 
            +
              data.tar.gz: 3736371f1c53590cf7e811c19fa7262e70470f50207d414087a08967bd3cdc7375f93a9256906df7ce44bb10aa14b887045da11144791169ca6a861cf19e02c3
         | 
    
        data/Gemfile.lock
    CHANGED
    
    
| @@ -1,8 +1,24 @@ | |
| 1 1 | 
             
            module BuilderApm
         | 
| 2 | 
            -
                class DashboardController <  | 
| 3 | 
            -
             | 
| 2 | 
            +
                class DashboardController < ApplicationController
         | 
| 3 | 
            +
                  def index
         | 
| 4 | 
            +
                    if params[:clear] == 'true'
         | 
| 5 | 
            +
                      keys = redis_client.zrange("builder_apm:timestamps", 0, -1)
         | 
| 6 | 
            +
              
         | 
| 7 | 
            +
                      keys = keys.map { |key| "builder_apm:Request:#{key}" }
         | 
| 8 | 
            +
                      # Delete base keys
         | 
| 9 | 
            +
                      redis_client.pipelined do |pipeline|
         | 
| 10 | 
            +
                        pipeline.del("builder_apm:timestamps")
         | 
| 11 | 
            +
                        pipeline.del("builder_apm:errors")
         | 
| 12 | 
            +
                        pipeline.del("builder_apm:n_plus_one")
         | 
| 13 | 
            +
                        pipeline.del("builder_apm:slow")
         | 
| 14 | 
            +
                      end
         | 
| 15 | 
            +
                      debugger
         | 
| 16 | 
            +
                      # Delete keys in chunks
         | 
| 17 | 
            +
                      keys.each_slice(1000) do |key_chunk|
         | 
| 18 | 
            +
                        redis_client.del(*key_chunk) unless key_chunk.empty?
         | 
| 19 | 
            +
                      end
         | 
| 4 20 | 
             
                    end
         | 
| 21 | 
            +
                  end
         | 
| 5 22 | 
             
                end
         | 
| 6 | 
            -
            end
         | 
| 7 | 
            -
             | 
| 23 | 
            +
              end
         | 
| 8 24 |  | 
| @@ -1,40 +1,41 @@ | |
| 1 1 | 
             
            module BuilderApm
         | 
| 2 | 
            -
             | 
| 3 | 
            -
             | 
| 4 | 
            -
             | 
| 5 | 
            -
             | 
| 6 | 
            -
                      
         | 
| 7 | 
            -
                        if cursor == "-inf"
         | 
| 8 | 
            -
                          @requests = retrieve_all_data_from_redis(limit)
         | 
| 9 | 
            -
                        else
         | 
| 10 | 
            -
                          @requests = retrieve_data_from_redis_since(cursor, limit)
         | 
| 11 | 
            -
                        end
         | 
| 12 | 
            -
                      
         | 
| 13 | 
            -
                        render json: @requests
         | 
| 14 | 
            -
                      end
         | 
| 2 | 
            +
              class RequestDataController < ApplicationController
         | 
| 3 | 
            +
                  def index
         | 
| 4 | 
            +
                      cursor = params[:cursor].presence&.to_f || Time.now.to_f
         | 
| 5 | 
            +
                      limit = params[:limit].presence&.to_i || 50
         | 
| 6 | 
            +
                      type = params[:type].presence || 'timestamps'
         | 
| 15 7 |  | 
| 16 | 
            -
             | 
| 8 | 
            +
                      purge_old_keys
         | 
| 9 | 
            +
                      @requests = retrieve_data_from_redis_since(cursor, limit, type)
         | 
| 17 10 |  | 
| 18 | 
            -
             | 
| 19 | 
            -
             | 
| 20 | 
            -
             | 
| 21 | 
            -
             | 
| 22 | 
            -
             | 
| 23 | 
            -
             | 
| 24 | 
            -
             | 
| 25 | 
            -
             | 
| 26 | 
            -
             | 
| 27 | 
            -
             | 
| 28 | 
            -
             | 
| 29 | 
            -
             | 
| 30 | 
            -
             | 
| 31 | 
            -
             | 
| 32 | 
            -
             | 
| 11 | 
            +
                      render json: @requests
         | 
| 12 | 
            +
                  end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  private
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  def purge_old_keys
         | 
| 17 | 
            +
                    time_limit = (Time.now - 1.hour).to_f * 1000
         | 
| 18 | 
            +
                    redis_client.zremrangebyscore('builder_apm:timestamps', '-inf', time_limit)
         | 
| 19 | 
            +
                    redis_client.zremrangebyscore('builder_apm:errors', '-inf', time_limit)
         | 
| 20 | 
            +
                    redis_client.zremrangebyscore('builder_apm:n_plus_one', '-inf', time_limit)
         | 
| 21 | 
            +
                    redis_client.zremrangebyscore('builder_apm:slow', '-inf', time_limit)
         | 
| 22 | 
            +
                  end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                  def retrieve_data_from_redis_since(cursor, limit, type)
         | 
| 25 | 
            +
                      # Use ZREVRANGEBYSCORE to get a range of keys, then get the corresponding data
         | 
| 26 | 
            +
                      # if(type == 'timestamps')
         | 
| 27 | 
            +
                        keys = redis_client.zrevrangebyscore("builder_apm:#{type}", cursor, "-inf", limit: [0, limit])
         | 
| 28 | 
            +
                      # else
         | 
| 29 | 
            +
                      #   keys = redis_client.lrange("builder_apm:#{type}", 0, -1)
         | 
| 30 | 
            +
                      # end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                      keys = keys.map { |key| "builder_apm:Request:#{key}" }
         | 
| 33 | 
            +
                      data = keys.empty? ? [] : redis_client.mget(*keys)
         | 
| 34 | 
            +
                      begin
         | 
| 33 35 | 
             
                        data.map { |d| JSON.parse(d) }
         | 
| 36 | 
            +
                      rescue => e
         | 
| 37 | 
            +
                        raise e
         | 
| 34 38 | 
             
                      end
         | 
| 35 | 
            -
             | 
| 36 | 
            -
             | 
| 37 | 
            -
             | 
| 38 | 
            -
                    end
         | 
| 39 | 
            -
                end
         | 
| 40 | 
            -
            end
         | 
| 39 | 
            +
                  end
         | 
| 40 | 
            +
              end
         | 
| 41 | 
            +
            end
         | 
| @@ -1,8 +1,11 @@ | |
| 1 1 | 
             
            module BuilderApm
         | 
| 2 | 
            -
                class RequestDetailsController <  | 
| 2 | 
            +
                class RequestDetailsController < ApplicationController
         | 
| 3 3 | 
             
                    def index
         | 
| 4 4 | 
             
                        @request_id = params[:request_id]
         | 
| 5 | 
            +
                        @data = redis_client.get("builder_apm:Request:#{@request_id}") || {}
         | 
| 6 | 
            +
                        
         | 
| 5 7 | 
             
                    end
         | 
| 8 | 
            +
             | 
| 6 9 | 
             
                end
         | 
| 7 10 | 
             
            end
         | 
| 8 11 |  | 
| @@ -64,7 +64,10 @@ body { | |
| 64 64 | 
             
                font-size: 1em; /* Adjust to match your anchors */
         | 
| 65 65 | 
             
                transition: background-color 0.3s ease, color 0.3s ease; /* Smooth transition */
         | 
| 66 66 | 
             
            }
         | 
| 67 | 
            -
             | 
| 67 | 
            +
            td.long_text{
         | 
| 68 | 
            +
              max-width:400px;
         | 
| 69 | 
            +
              overflow-wrap:break-word;
         | 
| 70 | 
            +
            }
         | 
| 68 71 | 
             
            th.sortable {
         | 
| 69 72 | 
             
                cursor: pointer; /* Makes the button cursor appear as a hand */
         | 
| 70 73 | 
             
            }
         | 
| @@ -6,12 +6,10 @@ google.charts.setOnLoadCallback(drawCharts); | |
| 6 6 |  | 
| 7 7 | 
             
            function drawCharts() {
         | 
| 8 8 | 
             
              prepareChartData(aggregationInterval);
         | 
| 9 | 
            -
             | 
| 10 | 
            -
             | 
| 11 | 
            -
             | 
| 12 | 
            -
             | 
| 13 | 
            -
                }
         | 
| 14 | 
            -
              }, 5000);
         | 
| 9 | 
            +
                autoFetchDataAndUpdateStorage(function(updatedData){
         | 
| 10 | 
            +
                  // Render the table with the updated data
         | 
| 11 | 
            +
                  prepareChartData(aggregationInterval)
         | 
| 12 | 
            +
              });
         | 
| 15 13 | 
             
            }
         | 
| 16 14 | 
             
            function prepareChartData(aggregationInterval) {
         | 
| 17 15 | 
             
              fetchDataAndUpdateStorage(function(updatedData) {
         | 
| @@ -6,53 +6,50 @@ var allData = null; | |
| 6 6 | 
             
            var isAsc = false;
         | 
| 7 7 | 
             
            var current_sort_field = 'real_start_time';
         | 
| 8 8 | 
             
            var start_time = Date.now();
         | 
| 9 | 
            +
            var data_gathered_counter = 0;
         | 
| 10 | 
            +
            var time_cursor = <%= Time.now.to_f * 1000 %>;
         | 
| 9 11 |  | 
| 10 12 | 
             
            function fetchDataAndUpdateStorage(onSuccess) {
         | 
| 11 13 | 
             
              start_time = Date.now();
         | 
| 12 | 
            -
              var initialCursor =  | 
| 13 | 
            -
               | 
| 14 | 
            -
              if (storedData) {
         | 
| 15 | 
            -
                var dataCompressed = decompress(storedData, function(result, error) {
         | 
| 16 | 
            -
                  allData = JSON.parse(result) || [];
         | 
| 17 | 
            -
                  fetchAndProcessData(initialCursor, onSuccess);
         | 
| 18 | 
            -
                });
         | 
| 19 | 
            -
              } else {
         | 
| 14 | 
            +
              var initialCursor = time_cursor || start_time;
         | 
| 15 | 
            +
              if(allData == null) {
         | 
| 20 16 | 
             
                  allData = [];
         | 
| 17 | 
            +
              }
         | 
| 21 18 | 
             
                // Start fetching data
         | 
| 22 19 | 
             
                fetchAndProcessData(initialCursor, onSuccess);
         | 
| 23 | 
            -
               | 
| 20 | 
            +
              
         | 
| 24 21 | 
             
            }
         | 
| 25 22 | 
             
            function fetchAndProcessData(cursor, onSuccess) {
         | 
| 26 | 
            -
                 | 
| 27 | 
            -
                var  | 
| 23 | 
            +
                var limit = 1000;
         | 
| 24 | 
            +
                var type = typeof fetch_type !== 'undefined' ? fetch_type : 'timestamps';
         | 
| 28 25 |  | 
| 29 26 | 
             
                // Make an AJAX request to fetch latest data
         | 
| 30 | 
            -
                $.get('/builder_apm/request_data?cursor=' + cursor + '&limit=' + limit, function(newData) {
         | 
| 31 | 
            -
             | 
| 27 | 
            +
                $.get('/builder_apm/request_data?cursor=' + cursor + '&limit=' + limit + '&type=' +type, function(newData) {
         | 
| 28 | 
            +
                  data_gathered_counter++;
         | 
| 32 29 | 
             
                  newData = newData.map(request => calcDurations(request));
         | 
| 33 30 |  | 
| 34 31 | 
             
                  processData(allData, null, newData, onSuccess);
         | 
| 35 32 |  | 
| 36 33 | 
             
                  // Find the latest request in the newly fetched data
         | 
| 37 | 
            -
                  var latestRequest = newData.reduce((latest, current) => current.real_end_time > latest.real_end_time ? current : latest, newData[0]);
         | 
| 34 | 
            +
                  // var latestRequest = newData.reduce((latest, current) => current.real_end_time > latest.real_end_time ? current : latest, newData[0]);
         | 
| 38 35 |  | 
| 39 36 | 
             
                  // If latest request's end_time is less than start_time, and there's more data (received data is equal to limit), fetch next batch
         | 
| 40 | 
            -
                  if (latestRequest && latestRequest.real_end_time < start_time && newData.length === limit) {
         | 
| 41 | 
            -
             | 
| 42 | 
            -
                  } else {
         | 
| 43 | 
            -
             | 
| 44 | 
            -
             | 
| 45 | 
            -
             | 
| 46 | 
            -
             | 
| 47 | 
            -
             | 
| 48 | 
            -
             | 
| 49 | 
            -
             | 
| 50 | 
            -
             | 
| 51 | 
            -
             | 
| 52 | 
            -
             | 
| 53 | 
            -
             | 
| 54 | 
            -
                    onSuccess(allData);
         | 
| 55 | 
            -
                   | 
| 37 | 
            +
                  // if (data_gathered_counter < 5 && latestRequest && latestRequest.real_end_time < start_time && newData.length === limit) {
         | 
| 38 | 
            +
                  //   fetchAndProcessData(var time_cursor = <%= Time.now.to_f * 1000 %>;, onSuccess);
         | 
| 39 | 
            +
                  // } else {
         | 
| 40 | 
            +
                  //   try {
         | 
| 41 | 
            +
                  //     // Store the updated data back to local storage
         | 
| 42 | 
            +
                  //     compress(JSON.stringify(allData), function(result, error) {
         | 
| 43 | 
            +
                  //       if(result != null) {
         | 
| 44 | 
            +
                  //         localStorage.setItem('builder_apm_requests', result);
         | 
| 45 | 
            +
                  //       }
         | 
| 46 | 
            +
                  //     });
         | 
| 47 | 
            +
                  //   } catch(e) {
         | 
| 48 | 
            +
                  //       console.error("Error storing data to local storage. Might be out of storage space.", e);
         | 
| 49 | 
            +
                  //       alert('not enough local storage')
         | 
| 50 | 
            +
                  //   }
         | 
| 51 | 
            +
                    // onSuccess(allData);
         | 
| 52 | 
            +
                  
         | 
| 56 53 | 
             
                });
         | 
| 57 54 | 
             
              }
         | 
| 58 55 |  | 
| @@ -81,8 +78,7 @@ function processData(storedData, error, newData, onSuccess) { | |
| 81 78 | 
             
                  // Update the cursor
         | 
| 82 79 | 
             
                  if (newData.length > 0) {
         | 
| 83 80 | 
             
                    var lastRequest = newData[newData.length - 1];
         | 
| 84 | 
            -
                     | 
| 85 | 
            -
                    localStorage.setItem('builder_apm_cursor', cursor.toString());
         | 
| 81 | 
            +
                    time_cursor = lastRequest.real_end_time-0.001;
         | 
| 86 82 | 
             
                  }
         | 
| 87 83 | 
             
                } catch(e) {
         | 
| 88 84 | 
             
                    console.error("Error storing data to local storage. Might be out of storage space.", e);
         | 
| @@ -101,6 +97,24 @@ function loadRequest(request_id) { | |
| 101 97 | 
             
                return requestData;
         | 
| 102 98 | 
             
            }
         | 
| 103 99 |  | 
| 100 | 
            +
            function autoFetchDataAndUpdateStorage(onSuccess) {
         | 
| 101 | 
            +
                var autoUpdate = $('#autoUpdate').is(':checked');
         | 
| 102 | 
            +
                if (autoUpdate) {
         | 
| 103 | 
            +
                    fetchDataAndUpdateStorage(function(updatedData) {
         | 
| 104 | 
            +
                        onSuccess(updatedData);
         | 
| 105 | 
            +
                        // Schedule the next run
         | 
| 106 | 
            +
                        setTimeout(function() { 
         | 
| 107 | 
            +
                            autoFetchDataAndUpdateStorage(onSuccess); 
         | 
| 108 | 
            +
                        }, 5000);
         | 
| 109 | 
            +
                    });
         | 
| 110 | 
            +
                } else {
         | 
| 111 | 
            +
                    // If auto update is not checked, schedule the next check
         | 
| 112 | 
            +
                    setTimeout(function() { 
         | 
| 113 | 
            +
                        autoFetchDataAndUpdateStorage(onSuccess); 
         | 
| 114 | 
            +
                    }, 5000);
         | 
| 115 | 
            +
                }
         | 
| 116 | 
            +
            }
         | 
| 117 | 
            +
             | 
| 104 118 | 
             
            function processStack(stack, processSqlEvent) {
         | 
| 105 119 | 
             
                if(stack == null) {
         | 
| 106 120 | 
             
                  stack = {}
         | 
| @@ -159,8 +173,12 @@ function tagQueriesWithNPlusOne(requestInput) { | |
| 159 173 | 
             
            function addSortingClick() {
         | 
| 160 174 | 
             
              // Assuming your table headers have a class name "sortable"
         | 
| 161 175 | 
             
              $('.sortable').click(function() {
         | 
| 176 | 
            +
                if (current_sort_field == $(this).data('field')) {
         | 
| 177 | 
            +
                  isAsc = !isAsc;
         | 
| 178 | 
            +
                } else {
         | 
| 179 | 
            +
                  isAsc = false;
         | 
| 180 | 
            +
                }
         | 
| 162 181 | 
             
                current_sort_field = $(this).data('field');  // Assuming data-field attribute contains the name of the field to sort
         | 
| 163 | 
            -
                isAsc = !isAsc;
         | 
| 164 182 |  | 
| 165 183 | 
             
                renderTable(allData);
         | 
| 166 184 | 
             
                });
         | 
| @@ -202,10 +220,12 @@ function getNPlusOneRequests(requestArray) { | |
| 202 220 | 
             
            function aggregateRequests(requestArray) {
         | 
| 203 221 | 
             
                let aggregates = {};
         | 
| 204 222 | 
             
                requestArray.forEach(request => {
         | 
| 205 | 
            -
                    let key = `${request.controller}#${request.action} | 
| 223 | 
            +
                    let key = `${request.controller}#${request.action}`;
         | 
| 206 224 | 
             
                    if (!aggregates[key]) {
         | 
| 207 225 | 
             
                        aggregates[key] = {
         | 
| 208 226 | 
             
                            count: 0,
         | 
| 227 | 
            +
                            method: request.method,
         | 
| 228 | 
            +
                            path: request.path,
         | 
| 209 229 | 
             
                            totalDuration: 0,
         | 
| 210 230 | 
             
                            totalDbRuntime: 0,
         | 
| 211 231 | 
             
                            totalViewRuntime: 0,
         | 
| @@ -214,6 +234,7 @@ function aggregateRequests(requestArray) { | |
| 214 234 | 
             
                            slowestViewRuntime: 0
         | 
| 215 235 | 
             
                        };
         | 
| 216 236 | 
             
                    }
         | 
| 237 | 
            +
                    
         | 
| 217 238 | 
             
                    aggregates[key].count++;
         | 
| 218 239 | 
             
                    aggregates[key].totalDuration += request.duration;
         | 
| 219 240 | 
             
                    aggregates[key].totalDbRuntime += request.db_runtime || 0;
         | 
| @@ -225,11 +246,11 @@ function aggregateRequests(requestArray) { | |
| 225 246 |  | 
| 226 247 | 
             
                let results = [];
         | 
| 227 248 | 
             
                for (let key in aggregates) {
         | 
| 228 | 
            -
                    let  | 
| 249 | 
            +
                    let controller = key;
         | 
| 229 250 | 
             
                    results.push({
         | 
| 230 251 | 
             
                        controller,
         | 
| 231 | 
            -
                        method,
         | 
| 232 | 
            -
                        path,
         | 
| 252 | 
            +
                        method: aggregates[key].method,
         | 
| 253 | 
            +
                        path: aggregates[key].path,
         | 
| 233 254 | 
             
                        count: aggregates[key].count,
         | 
| 234 255 | 
             
                        averageDuration: aggregates[key].totalDuration / aggregates[key].count,
         | 
| 235 256 | 
             
                        averageDbRuntime: aggregates[key].totalDbRuntime / aggregates[key].count,
         | 
| @@ -1,24 +1,27 @@ | |
| 1 1 | 
             
            <script>
         | 
| 2 | 
            +
            var fetch_type = 'errors';
         | 
| 2 3 | 
             
            $(document).ready(function() {
         | 
| 3 | 
            -
             | 
| 4 | 
            -
             | 
| 5 | 
            -
             | 
| 4 | 
            +
              var storedData = localStorage.getItem('builder_apm_requests');
         | 
| 5 | 
            +
              if (storedData) {
         | 
| 6 | 
            +
                var dataCompressed = decompress(storedData, function(result, error) {
         | 
| 7 | 
            +
                  allData = JSON.parse(result) || [];
         | 
| 8 | 
            +
                  renderTable(allData);
         | 
| 6 9 | 
             
                });
         | 
| 10 | 
            +
              }
         | 
| 7 11 |  | 
| 8 | 
            -
             | 
| 9 | 
            -
             | 
| 10 | 
            -
             | 
| 11 | 
            -
             | 
| 12 | 
            -
             | 
| 13 | 
            -
             | 
| 14 | 
            -
                   | 
| 15 | 
            -
             | 
| 12 | 
            +
              fetchDataAndUpdateStorage(function(updatedData) {
         | 
| 13 | 
            +
                renderTable(updatedData);
         | 
| 14 | 
            +
              });
         | 
| 15 | 
            +
             | 
| 16 | 
            +
              autoFetchDataAndUpdateStorage(function(updatedData){
         | 
| 17 | 
            +
                  // Render the table with the updated data
         | 
| 18 | 
            +
                  renderTable(updatedData);
         | 
| 19 | 
            +
              });
         | 
| 16 20 |  | 
| 17 21 | 
             
                addSortingClick();
         | 
| 18 22 | 
             
              });
         | 
| 19 23 |  | 
| 20 24 | 
             
              function renderTable(data, target = null) {
         | 
| 21 | 
            -
                data = data.filter(item => item.status === 500);
         | 
| 22 25 | 
             
                data = sortDataBy(current_sort_field, data);
         | 
| 23 26 |  | 
| 24 27 | 
             
                // Get a reference to the table body
         | 
| @@ -27,12 +30,17 @@ $(document).ready(function() { | |
| 27 30 | 
             
                // Clear the table body
         | 
| 28 31 | 
             
                tableBody.empty();
         | 
| 29 32 |  | 
| 33 | 
            +
               if(data.length == 0) {
         | 
| 34 | 
            +
                  var messageElement = $('<p>').text('No 500 Requests found');
         | 
| 35 | 
            +
                  tableBody.after(messageElement);
         | 
| 36 | 
            +
                  return;
         | 
| 37 | 
            +
                }
         | 
| 30 38 | 
             
                // Create new table rows for each data item
         | 
| 31 39 | 
             
                data.forEach(function(item) {
         | 
| 32 40 | 
             
                  var row = $('<tr>');
         | 
| 33 41 |  | 
| 34 42 | 
             
                  $('<td>').text(new Date(item['start_time']).toLocaleString()).appendTo(row);
         | 
| 35 | 
            -
                  $('<td>').text(item['controller'] + '#' + item['action']).appendTo(row);
         | 
| 43 | 
            +
                  $('<td>').addClass('long_text').text(item['controller'] + '#' + item['action']).appendTo(row);
         | 
| 36 44 | 
             
                  $('<td>').text(item['method']).appendTo(row);
         | 
| 37 45 | 
             
                  $('<td>').text(item['status']).appendTo(row);
         | 
| 38 46 | 
             
                  $('<td>').append(item['exception_class']).appendTo(row);
         | 
| @@ -1,23 +1,26 @@ | |
| 1 1 | 
             
            <script>
         | 
| 2 | 
            +
            var fetch_type = 'n_plus_one';
         | 
| 2 3 | 
             
            $(document).ready(function() {
         | 
| 4 | 
            +
              var storedData = localStorage.getItem('builder_apm_requests');
         | 
| 5 | 
            +
              if (storedData) {
         | 
| 6 | 
            +
                var dataCompressed = decompress(storedData, function(result, error) {
         | 
| 7 | 
            +
                  allData = JSON.parse(result) || [];
         | 
| 8 | 
            +
                  renderTable(allData);
         | 
| 9 | 
            +
                });
         | 
| 10 | 
            +
              }
         | 
| 3 11 | 
             
                fetchDataAndUpdateStorage(function(updatedData) {
         | 
| 4 12 | 
             
                  renderTable(updatedData);
         | 
| 5 13 | 
             
                });
         | 
| 6 14 |  | 
| 7 | 
            -
             | 
| 8 | 
            -
                   | 
| 9 | 
            -
                   | 
| 10 | 
            -
             | 
| 11 | 
            -
                        renderTable(updatedData);
         | 
| 12 | 
            -
                    });
         | 
| 13 | 
            -
                  }
         | 
| 14 | 
            -
                }, 5000);
         | 
| 15 | 
            +
              autoFetchDataAndUpdateStorage(function(updatedData){
         | 
| 16 | 
            +
                  // Render the table with the updated data
         | 
| 17 | 
            +
                  renderTable(updatedData);
         | 
| 18 | 
            +
              });
         | 
| 15 19 |  | 
| 16 20 | 
             
                addSortingClick();
         | 
| 17 21 | 
             
              });
         | 
| 18 22 |  | 
| 19 23 | 
             
              function renderTable(data, target = null) {
         | 
| 20 | 
            -
                data = detectNPlusOne(data);
         | 
| 21 24 | 
             
                data = sortDataBy(current_sort_field, data);
         | 
| 22 25 |  | 
| 23 26 | 
             
                // Get a reference to the table body
         | 
| @@ -26,12 +29,17 @@ $(document).ready(function() { | |
| 26 29 | 
             
                // Clear the table body
         | 
| 27 30 | 
             
                tableBody.empty();
         | 
| 28 31 |  | 
| 32 | 
            +
               if(data.length == 0) {
         | 
| 33 | 
            +
                  var messageElement = $('<p>').text('No N+1 Requests found');
         | 
| 34 | 
            +
                  tableBody.after(messageElement);
         | 
| 35 | 
            +
                  return;
         | 
| 36 | 
            +
                }
         | 
| 29 37 | 
             
                // Create new table rows for each data item
         | 
| 30 38 | 
             
                data.forEach(function(item) {
         | 
| 31 39 | 
             
                  var row = $('<tr>');
         | 
| 32 40 |  | 
| 33 41 | 
             
                  $('<td>').text(new Date(item['start_time']).toLocaleString()).appendTo(row);
         | 
| 34 | 
            -
                  $('<td>').text(item['controller'] + '#' + item['action']).appendTo(row);
         | 
| 42 | 
            +
                  $('<td>').addClass('long_text').text(item['controller'] + '#' + item['action']).appendTo(row);
         | 
| 35 43 | 
             
                  $('<td>').text(item['method']).appendTo(row);
         | 
| 36 44 | 
             
                  $('<td>').text(item['status']).appendTo(row);
         | 
| 37 45 | 
             
                  $('<td>').append(renderDuration(item['duration'])).appendTo(row);
         | 
| @@ -1,26 +1,27 @@ | |
| 1 1 | 
             
            <script>
         | 
| 2 2 |  | 
| 3 3 | 
             
            $(document).ready(function() {
         | 
| 4 | 
            +
              var storedData = localStorage.getItem('builder_apm_requests');
         | 
| 5 | 
            +
              if (storedData) {
         | 
| 6 | 
            +
                var dataCompressed = decompress(storedData, function(result, error) {
         | 
| 7 | 
            +
                  allData = JSON.parse(result) || [];
         | 
| 8 | 
            +
                  renderTable(allData);
         | 
| 9 | 
            +
                });
         | 
| 10 | 
            +
              }
         | 
| 4 11 | 
             
                fetchDataAndUpdateStorage(function(updatedData) {
         | 
| 5 12 | 
             
                  // Render the table with the updated data
         | 
| 6 13 | 
             
                  renderTable(updatedData);
         | 
| 7 14 | 
             
                });
         | 
| 8 15 |  | 
| 9 16 |  | 
| 10 | 
            -
             | 
| 11 | 
            -
                   | 
| 12 | 
            -
                   | 
| 13 | 
            -
             | 
| 14 | 
            -
                      // Render the table with the updated data
         | 
| 15 | 
            -
                      renderTable(updatedData);
         | 
| 16 | 
            -
                    });
         | 
| 17 | 
            -
                  }
         | 
| 18 | 
            -
                }, 5000);
         | 
| 17 | 
            +
              autoFetchDataAndUpdateStorage(function(updatedData){
         | 
| 18 | 
            +
                  // Render the table with the updated data
         | 
| 19 | 
            +
                  renderTable(updatedData);
         | 
| 20 | 
            +
              });
         | 
| 19 21 |  | 
| 20 22 | 
             
                addSortingClick();
         | 
| 21 23 | 
             
              });
         | 
| 22 24 |  | 
| 23 | 
            -
              
         | 
| 24 25 | 
             
              function renderTable(data, target = null) {
         | 
| 25 26 | 
             
                data = sortDataBy(current_sort_field, data);
         | 
| 26 27 |  | 
| @@ -30,12 +31,18 @@ $(document).ready(function() { | |
| 30 31 | 
             
                // Clear the table body
         | 
| 31 32 | 
             
                tableBody.empty();
         | 
| 32 33 |  | 
| 34 | 
            +
              
         | 
| 35 | 
            +
               if(data.length == 0) {
         | 
| 36 | 
            +
                  var messageElement = $('<p>').text('No Requests found');
         | 
| 37 | 
            +
                  tableBody.after(messageElement);
         | 
| 38 | 
            +
                  return;
         | 
| 39 | 
            +
                }
         | 
| 33 40 | 
             
                // Create new table rows for each data item
         | 
| 34 41 | 
             
                data.forEach(function(item) {
         | 
| 35 42 | 
             
                  var row = $('<tr>');
         | 
| 36 43 |  | 
| 37 44 | 
             
                  $('<td>').text(new Date(item['start_time']).toLocaleString()).appendTo(row);
         | 
| 38 | 
            -
                  $('<td>').text(item['controller'] + '#' + item['action']).appendTo(row);
         | 
| 45 | 
            +
                  $('<td>').addClass('long_text').text(item['controller'] + '#' + item['action']).appendTo(row);
         | 
| 39 46 | 
             
                  $('<td>').text(item['method']).appendTo(row);
         | 
| 40 47 | 
             
                  $('<td>').text(item['status']).appendTo(row);
         | 
| 41 48 | 
             
                  $('<td>').append(renderDuration(item['real_duration_time'])).appendTo(row);
         | 
| @@ -46,13 +53,10 @@ $(document).ready(function() { | |
| 46 53 |  | 
| 47 54 | 
             
                  if(item.stack && item.stack.length > 0) {
         | 
| 48 55 |  | 
| 49 | 
            -
                    var  | 
| 50 | 
            -
                     | 
| 51 | 
            -
             | 
| 52 | 
            -
             | 
| 53 | 
            -
                      window.location.href = '<%= request_details_path %>?request_id=' + item['request_id'];
         | 
| 54 | 
            -
                    });
         | 
| 55 | 
            -
                    actionButton.appendTo(actionTd);
         | 
| 56 | 
            +
                    var actionLink = $('<a>').text('Details');
         | 
| 57 | 
            +
                    actionLink.attr('href', '<%= request_details_path %>?request_id=' + item['request_id']);
         | 
| 58 | 
            +
                    actionLink.attr('target', '_blank');
         | 
| 59 | 
            +
                    actionLink.appendTo(actionTd);
         | 
| 56 60 | 
             
                  }
         | 
| 57 61 | 
             
                  actionTd.appendTo(row);
         | 
| 58 62 |  | 
| @@ -1,17 +1,21 @@ | |
| 1 1 | 
             
            <script>
         | 
| 2 2 | 
             
            $(document).ready(function() {
         | 
| 3 | 
            +
              var storedData = localStorage.getItem('builder_apm_requests');
         | 
| 4 | 
            +
              if (storedData) {
         | 
| 5 | 
            +
                var dataCompressed = decompress(storedData, function(result, error) {
         | 
| 6 | 
            +
                  allData = JSON.parse(result) || [];
         | 
| 7 | 
            +
                  renderTable(allData);
         | 
| 8 | 
            +
                });
         | 
| 9 | 
            +
              }
         | 
| 10 | 
            +
             | 
| 3 11 | 
             
                fetchDataAndUpdateStorage(function(updatedData) {
         | 
| 4 12 | 
             
                  renderTable(updatedData);
         | 
| 5 13 | 
             
                });
         | 
| 6 14 |  | 
| 7 | 
            -
             | 
| 8 | 
            -
                   | 
| 9 | 
            -
                   | 
| 10 | 
            -
             | 
| 11 | 
            -
                        renderTable(updatedData);
         | 
| 12 | 
            -
                    });
         | 
| 13 | 
            -
                  }
         | 
| 14 | 
            -
                }, 5000);
         | 
| 15 | 
            +
              autoFetchDataAndUpdateStorage(function(updatedData){
         | 
| 16 | 
            +
                  // Render the table with the updated data
         | 
| 17 | 
            +
                  renderTable(updatedData);
         | 
| 18 | 
            +
              });
         | 
| 15 19 |  | 
| 16 20 | 
             
                addSortingClick();
         | 
| 17 21 | 
             
              });
         | 
| @@ -26,13 +30,18 @@ $(document).ready(function() { | |
| 26 30 | 
             
                // Clear the table body
         | 
| 27 31 | 
             
                tableBody.empty();
         | 
| 28 32 |  | 
| 33 | 
            +
               if(data.length == 0) {
         | 
| 34 | 
            +
                  var messageElement = $('<p>').text('No Requests found');
         | 
| 35 | 
            +
                  tableBody.after(messageElement);
         | 
| 36 | 
            +
                  return;
         | 
| 37 | 
            +
                }
         | 
| 29 38 | 
             
                // Create new table rows for each data item
         | 
| 30 39 | 
             
                data.forEach(function(item) {
         | 
| 31 40 | 
             
                  var row = $('<tr>');
         | 
| 32 41 |  | 
| 33 | 
            -
                  $('<td>').text(item['controller']).appendTo(row);
         | 
| 42 | 
            +
                  $('<td>').addClass('long_text').text(item['controller']).appendTo(row);
         | 
| 34 43 | 
             
                  $('<td>').text(item['method']).appendTo(row);
         | 
| 35 | 
            -
                  $('<td>').text(item['path']).appendTo(row);
         | 
| 44 | 
            +
                  $('<td>').addClass('long_text').text(item['path']).appendTo(row);
         | 
| 36 45 | 
             
                  $('<td>').text(item['count']).appendTo(row);
         | 
| 37 46 | 
             
                  $('<td>').append(renderDuration(item['averageDuration'])).appendTo(row);
         | 
| 38 47 | 
             
                  $('<td>').append(renderDuration(item['averageDbRuntime'])).appendTo(row);
         | 
| @@ -1,31 +1,19 @@ | |
| 1 1 | 
             
            <script>
         | 
| 2 2 | 
             
            var request_id = '<%= @request_id %>';
         | 
| 3 | 
            +
            var request_data = <%= @data.to_json.html_safe %>;
         | 
| 3 4 | 
             
            var current_index = 0;
         | 
| 4 5 |  | 
| 5 6 | 
             
            $(document).ready(function() {
         | 
| 6 7 | 
             
                prepPage();
         | 
| 7 | 
            -
                 | 
| 8 | 
            -
             | 
| 9 | 
            -
             | 
| 10 | 
            -
             | 
| 11 | 
            -
             | 
| 12 | 
            -
             | 
| 13 | 
            -
                     | 
| 14 | 
            -
             | 
| 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 | 
            -
                });
         | 
| 8 | 
            +
                var request = JSON.parse(request_data);
         | 
| 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));
         | 
| 29 17 | 
             
            });
         | 
| 30 18 |  | 
| 31 19 | 
             
            function prepPage(){
         | 
| @@ -1,23 +1,27 @@ | |
| 1 1 | 
             
            <script>
         | 
| 2 | 
            +
            var fetch_type = 'slow';
         | 
| 2 3 | 
             
            $(document).ready(function() {
         | 
| 4 | 
            +
              var storedData = localStorage.getItem('builder_apm_requests');
         | 
| 5 | 
            +
              if (storedData) {
         | 
| 6 | 
            +
                var dataCompressed = decompress(storedData, function(result, error) {
         | 
| 7 | 
            +
                  allData = JSON.parse(result) || [];
         | 
| 8 | 
            +
                  renderTable(allData);
         | 
| 9 | 
            +
                });
         | 
| 10 | 
            +
              }
         | 
| 11 | 
            +
             | 
| 3 12 | 
             
                fetchDataAndUpdateStorage(function(updatedData) {
         | 
| 4 13 | 
             
                  renderTable(updatedData);
         | 
| 5 14 | 
             
                });
         | 
| 6 15 |  | 
| 7 | 
            -
             | 
| 8 | 
            -
                   | 
| 9 | 
            -
                   | 
| 10 | 
            -
             | 
| 11 | 
            -
                    renderTable(updatedData);
         | 
| 12 | 
            -
                    });
         | 
| 13 | 
            -
                  }
         | 
| 14 | 
            -
                }, 5000);
         | 
| 16 | 
            +
              autoFetchDataAndUpdateStorage(function(updatedData){
         | 
| 17 | 
            +
                  // Render the table with the updated data
         | 
| 18 | 
            +
                  renderTable(updatedData);
         | 
| 19 | 
            +
              });
         | 
| 15 20 |  | 
| 16 21 | 
             
                addSortingClick();
         | 
| 17 22 | 
             
              });
         | 
| 18 23 |  | 
| 19 24 | 
             
              function renderTable(data, target = null) {
         | 
| 20 | 
            -
                data = data.filter(item => item.duration > 1500);
         | 
| 21 25 | 
             
                data = sortDataBy(current_sort_field, data);
         | 
| 22 26 |  | 
| 23 27 | 
             
                // Get a reference to the table body
         | 
| @@ -26,12 +30,17 @@ $(document).ready(function() { | |
| 26 30 | 
             
                // Clear the table body
         | 
| 27 31 | 
             
                tableBody.empty();
         | 
| 28 32 |  | 
| 33 | 
            +
               if(data.length == 0) {
         | 
| 34 | 
            +
                  var messageElement = $('<p>').text('No Slow Requests found');
         | 
| 35 | 
            +
                  tableBody.after(messageElement);
         | 
| 36 | 
            +
                  return;
         | 
| 37 | 
            +
                }
         | 
| 29 38 | 
             
                // Create new table rows for each data item
         | 
| 30 39 | 
             
                data.forEach(function(item) {
         | 
| 31 40 | 
             
                  var row = $('<tr>');
         | 
| 32 41 |  | 
| 33 42 | 
             
                  $('<td>').text(new Date(item['start_time']).toLocaleString()).appendTo(row);
         | 
| 34 | 
            -
                  $('<td>').text(item['controller'] + '#' + item['action']).appendTo(row);
         | 
| 43 | 
            +
                  $('<td>').addClass('long_text').text(item['controller'] + '#' + item['action']).appendTo(row);
         | 
| 35 44 | 
             
                  $('<td>').text(item['method']).appendTo(row);
         | 
| 36 45 | 
             
                  $('<td>').text(item['status']).appendTo(row);
         | 
| 37 46 | 
             
                  $('<td>').append(renderDuration(item['duration'])).appendTo(row);
         | 
| @@ -46,7 +46,6 @@ $(document).ready(function() { | |
| 46 46 | 
             
                  <li><%= link_to 'Slow Requests', slow_requests_path, class: ("active" if current_page?(slow_requests_path)) %></li>
         | 
| 47 47 | 
             
                  <li><%= link_to 'N+1', n_plus_one_path, class: ("active" if current_page?(n_plus_one_path)) %></li>
         | 
| 48 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 49 | 
             
                </ul>
         | 
| 51 50 | 
             
              </nav>
         | 
| 52 51 | 
             
              <div id="options">
         | 
| @@ -4,12 +4,14 @@ module BuilderApm | |
| 4 4 | 
             
                attr_accessor :enable_controller_profiler
         | 
| 5 5 | 
             
                attr_accessor :enable_active_record_profiler
         | 
| 6 6 | 
             
                attr_accessor :enable_methods_profiler
         | 
| 7 | 
            +
                attr_accessor :enable_n_plus_one_profiler
         | 
| 7 8 |  | 
| 8 9 | 
             
                def initialize
         | 
| 9 10 | 
             
                  @redis_url = 'redis://localhost:6379/0'
         | 
| 10 11 | 
             
                  @enable_controller_profiler = true
         | 
| 11 12 | 
             
                  @enable_active_record_profiler = true
         | 
| 12 13 | 
             
                  @enable_methods_profiler = true
         | 
| 14 | 
            +
                  @enable_n_plus_one_profiler = true
         | 
| 13 15 | 
             
                end
         | 
| 14 16 | 
             
              end
         | 
| 15 17 | 
             
            end
         | 
| @@ -9,6 +9,8 @@ module BuilderApm | |
| 9 9 | 
             
                  def call(env)
         | 
| 10 10 | 
             
                    request_id = env["action_dispatch.request_id"]
         | 
| 11 11 | 
             
                    Thread.current["request_id"] = request_id
         | 
| 12 | 
            +
                    Thread.current[:n_plus_one_duration] = 0
         | 
| 13 | 
            +
                    Thread.current[:has_n_plus_one] = false
         | 
| 12 14 | 
             
                    start_time = Time.now.to_f * 1000
         | 
| 13 15 |  | 
| 14 16 | 
             
                    @status, @headers, @response = @app.call(env)
         | 
| @@ -18,6 +20,10 @@ module BuilderApm | |
| 18 20 | 
             
                    end_time = Time.now.to_f * 1000
         | 
| 19 21 | 
             
                    handle_timing(start_time, end_time, request_id)
         | 
| 20 22 |  | 
| 23 | 
            +
                    Thread.current['request_data'] = nil
         | 
| 24 | 
            +
                    Thread.current[:has_n_plus_one] = nil
         | 
| 25 | 
            +
                    Thread.current[:n_plus_one_duration] = nil
         | 
| 26 | 
            +
             | 
| 21 27 | 
             
                    [@status, @headers, @response]
         | 
| 22 28 | 
             
                  end
         | 
| 23 29 |  | 
| @@ -28,14 +34,25 @@ module BuilderApm | |
| 28 34 | 
             
                    data = Thread.current['request_data']
         | 
| 29 35 |  | 
| 30 36 | 
             
                    if data
         | 
| 37 | 
            +
                      data[:has_n_plus_one] = Thread.current[:has_n_plus_one]
         | 
| 38 | 
            +
                      data[:n_plus_one_duration] = Thread.current[:n_plus_one_duration]
         | 
| 31 39 | 
             
                      data[:real_start_time] = start_time 
         | 
| 32 40 | 
             
                      data[:real_end_time] = end_time
         | 
| 33 41 | 
             
                      data[:real_duration_time] = end_time - start_time
         | 
| 34 | 
            -
             | 
| 35 | 
            -
                      
         | 
| 42 | 
            +
             | 
| 36 43 | 
             
                      begin
         | 
| 37 | 
            -
                        @redis_client. | 
| 38 | 
            -
             | 
| 44 | 
            +
                        @redis_client.pipelined do |pipeline|
         | 
| 45 | 
            +
                          pipeline.rpush("builder_apm:Analysis:#{data[:controller]}##{data[:action]}:duration", data[:real_duration_time]||0)
         | 
| 46 | 
            +
                          pipeline.rpush("builder_apm:Analysis:#{data[:controller]}##{data[:action]}:db_runtime", data[:db_runtime]||0)
         | 
| 47 | 
            +
                          pipeline.rpush("builder_apm:Analysis:#{data[:controller]}##{data[:action]}:view_runtime", data[:view_runtime]||0)
         | 
| 48 | 
            +
                        
         | 
| 49 | 
            +
                          pipeline.zadd("builder_apm:timestamps", end_time, request_id)
         | 
| 50 | 
            +
                          pipeline.zadd("builder_apm:errors", end_time, request_id) if data[:status] == 500
         | 
| 51 | 
            +
                          pipeline.zadd("builder_apm:n_plus_one", end_time, request_id) if data[:has_n_plus_one]
         | 
| 52 | 
            +
                          pipeline.zadd("builder_apm:slow", end_time, request_id) if data[:real_duration_time] > 1500
         | 
| 53 | 
            +
                          ttl = (1.hour + 1.minute).to_i
         | 
| 54 | 
            +
                          pipeline.set("builder_apm:Request:#{data[:request_id]}", data.to_json, ex: ttl)
         | 
| 55 | 
            +
                        end
         | 
| 39 56 | 
             
                      rescue => e
         | 
| 40 57 | 
             
                        Rails.logger.error "Redis Missing"
         | 
| 41 58 | 
             
                      end
         | 
| @@ -54,14 +54,25 @@ module BuilderApm | |
| 54 54 | 
             
                  end
         | 
| 55 55 |  | 
| 56 56 | 
             
                  def store_sql_query_data(sql_query_data)
         | 
| 57 | 
            -
             | 
| 58 | 
            -
             | 
| 59 | 
            -
             | 
| 60 | 
            -
             | 
| 61 | 
            -
             | 
| 62 | 
            -
             | 
| 63 | 
            -
                     | 
| 57 | 
            +
                  Thread.current[:sql_event_id] = sql_query_data[:sql_id]
         | 
| 58 | 
            +
                  
         | 
| 59 | 
            +
                  # Create the stack if it doesn't exist yet
         | 
| 60 | 
            +
                  stack = (Thread.current[:stack] ||= [])
         | 
| 61 | 
            +
                  
         | 
| 62 | 
            +
                  if stack&.any?
         | 
| 63 | 
            +
                    stack.last[:sql_events].push(sql_query_data)
         | 
| 64 | 
            +
                  else
         | 
| 65 | 
            +
                    stack.push({sql_events: [sql_query_data], children: []})
         | 
| 64 66 | 
             
                  end
         | 
| 67 | 
            +
                  # Do the N+1 check if it wasn't done yet
         | 
| 68 | 
            +
                  if BuilderApm.configuration.enable_n_plus_one_profiler && Thread.current[:has_n_plus_one] == false
         | 
| 69 | 
            +
                    start_time = Time.now.to_f * 1000
         | 
| 70 | 
            +
                    perform_n_plus_one_check(stack)
         | 
| 71 | 
            +
                    duration = (Time.now.to_f * 1000) - start_time
         | 
| 72 | 
            +
                    Thread.current[:n_plus_one_duration] ||= 0
         | 
| 73 | 
            +
                    Thread.current[:n_plus_one_duration] += duration
         | 
| 74 | 
            +
                  end
         | 
| 75 | 
            +
                end
         | 
| 65 76 |  | 
| 66 77 | 
             
                  def update_last_sql_query_data_with_instantiation_info(event)
         | 
| 67 78 | 
             
                    stack = Thread.current[:stack]
         | 
| @@ -81,7 +92,31 @@ module BuilderApm | |
| 81 92 | 
             
                    ensure
         | 
| 82 93 | 
             
                      Thread.current[:sql_event_id] = nil
         | 
| 83 94 | 
             
                    end
         | 
| 84 | 
            -
                  end | 
| 95 | 
            +
                  end 
         | 
| 96 | 
            +
                  
         | 
| 97 | 
            +
                  # Add this method to perform the N+1 check
         | 
| 98 | 
            +
                  def perform_n_plus_one_check(stack)
         | 
| 99 | 
            +
                    sql_queries = stack.map { |frame| frame[:sql_events] }.flatten
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                    # Group queries count by table and triggering_line
         | 
| 102 | 
            +
                    queries_count = Hash.new { |h, k| h[k] = {count: 0, indices: []} }
         | 
| 103 | 
            +
                    sql_queries.each_with_index do |query, index|
         | 
| 104 | 
            +
                      match = query[:sql].match(/FROM ['`"]*([^ '`"]+)['`"]*/i)
         | 
| 105 | 
            +
                      if match
         | 
| 106 | 
            +
                        table_and_line = "#{match[1]}|#{query[:triggering_line]}"
         | 
| 107 | 
            +
                        queries_count[table_and_line][:count] += 1
         | 
| 108 | 
            +
                        queries_count[table_and_line][:indices].push(index)
         | 
| 109 | 
            +
                      end
         | 
| 110 | 
            +
                    end
         | 
| 111 | 
            +
             | 
| 112 | 
            +
                    # If any N+1 issue is found, set 'has_n_plus_one' to true and return
         | 
| 113 | 
            +
                    queries_count.each do |_, value|
         | 
| 114 | 
            +
                      if value[:count] > 1
         | 
| 115 | 
            +
                        Thread.current[:has_n_plus_one] = true
         | 
| 116 | 
            +
                        return
         | 
| 117 | 
            +
                      end
         | 
| 118 | 
            +
                    end
         | 
| 119 | 
            +
                  end
         | 
| 85 120 | 
             
                end
         | 
| 86 121 | 
             
              end
         | 
| 87 122 | 
             
            end
         | 
    
        data/lib/builder_apm/version.rb
    CHANGED
    
    
    
        metadata
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: builder_apm
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0. | 
| 4 | 
            +
              version: 0.3.0
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Paul Ketelle
         | 
| @@ -60,6 +60,7 @@ files: | |
| 60 60 | 
             
            - LICENSE.txt
         | 
| 61 61 | 
             
            - README.md
         | 
| 62 62 | 
             
            - Rakefile
         | 
| 63 | 
            +
            - app/controllers/builder_apm/application_controller.rb
         | 
| 63 64 | 
             
            - app/controllers/builder_apm/dashboard_controller.rb
         | 
| 64 65 | 
             
            - app/controllers/builder_apm/error_requests_controller.rb
         | 
| 65 66 | 
             
            - app/controllers/builder_apm/n_plus_one_controller.rb
         |