rack-mini-profiler 2.1.0 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1603a0c0246f9b132f81e74331425a1217156d62f71f20a06ecd3edca91aef51
4
- data.tar.gz: a341e35cf639c11cc61c9ef5fd1ecd25e89ff12da299d3a6bfe8cb479c5dd7d8
3
+ metadata.gz: 42690a86bcd4e4cfd8bf17289dc7b2f583bdd4ec286b4bbcdd50454f0f73551b
4
+ data.tar.gz: 723291f0ae196b09f585a331fc4e33b54bfd4be7fba527b623d851dc77524cce
5
5
  SHA512:
6
- metadata.gz: 4244e2738847e0e34b33cebda96f45d79ad82201ce969310bf45f1ad7431561af0d41610c2e8c60a0c64d428032a1e4bea6cc5b02aa71b5462a918474228844a
7
- data.tar.gz: b1c0d88134e780bbbb91f711366400585d51d98107d1f35af85ca7c38498f0e23de019f2eb05259e833d8deabf485dc70f1dd581c28e132aba68bfa785967774
6
+ metadata.gz: ca4bd7e3f35d42171bf2e39775a4a8fb565b907ba1eab40002886f7933bb4e9d30bfb8d9a87a7cb408e68b3f7e1a5dbbd360fa633f8e5c94793218abb40b32d5
7
+ data.tar.gz: 877164b6caf03efc6fa4183bed8f6dfce09c691e98c9ac3588fae8307d6b1632be8e666140c4a581280b6f0bfb69c2f5d4cb100ecca0f4afe1d3a9cd770b8e7f
@@ -1,5 +1,12 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 2.2.0 - 2020-10-19
4
+
5
+ - [UX] Enhancements to snapshots UI
6
+ - [FEATURE] Mini Profiler cookie is now sameSite=lax
7
+ - [FEATURE] Snapshots transporter
8
+ - [FEATURE] Redact SQL queries in snapshots by default
9
+
3
10
  ## 2.1.0 - 2020-09-17
4
11
 
5
12
  - [FEATURE] Allow assets to be precompiled with Sprockets
data/README.md CHANGED
@@ -198,6 +198,16 @@ Access to the snapshots page is restricted to only those who can see the speed b
198
198
 
199
199
  Mini Profiler will keep a maximum of 1000 snapshots by default, and you can change that via the `snapshots_limit` config. When snapshots reach the configured limit, Mini Profiler will save a new snapshot only if it's worse than at least one of the existing snapshots and delete the best one (i.e. the snapshot whose request took the least time compared to other snapshots).
200
200
 
201
+ #### Snapshots Transporter
202
+
203
+ Mini Profiler can be configured so that it sends snapshots over HTTP using the snapshots transporter. The main use-case of the transporter is to allow the aggregation of snapshots from multiple applications/sources in a single place. To enable the snapshots transporter, you need to provide a destination URL to the `snapshots_transport_destination_url` config, and a secure key to the `snapshots_transport_auth_key` config (will be used for authorization). Both of these configs are required for the transporter to be enabled.
204
+
205
+ The transporter uses a buffer to temporarily hold snapshots in memory with a limit of 100 snapshots. Every 10 seconds, *if* the buffer is not empty, the transporter will make a `POST` request with the buffer content to the destination URL. Requests made by the transporter will have a `Mini-Profiler-Transport-Auth` header with the value of the `snapshots_transport_auth_key` config. The destination should only accept requests that include this header AND the header's value matches the key you set to the `snapshots_transport_auth_key` config.
206
+
207
+ The body of the requests made by the transporter is a JSON string with a single top-level key called `snapshots` and it has an array of snapshots. The structure of a snapshot is too complex to be explained here, but it has the same structure that Mini Profiler client expects. So if your use-case is to simply be able to view snapshots from multiple sources in one place, you should simply store the snapshots as-is, and then serve them to Mini Profiler client to consume. If the destination application also has Mini Profiler, you can simply use the API of the storage backends to store the incoming snapshots and Mini Profiler will treat them the same as local snapshots (e.g. they'll be grouped and displayed in the same manner described in the previous section).
208
+
209
+ Mini Profiler offers an API to add extra fields (a.k.a custom fields) to snapshots. For example, you may want to add whether the request was made by a logged-in or anonymous user, the version of your application or any other things that are specific to your application. To add custom fields to a snapshot, call the `Rack::MiniProfiler.add_snapshot_custom_field(<key>, <value>)` method anywhere during the lifetime of a request, and the snapshot of that request will include the fields you added. If you have a Rails app, you can call that method in an `after_action` callback. Custom fields are cleared between requests.
210
+
201
211
  ## Access control in non-development environments
202
212
 
203
213
  rack-mini-profiler is designed with production profiling in mind. To enable that run `Rack::MiniProfiler.authorize_request` once you know a request is allowed to profile.
@@ -384,6 +394,10 @@ enable_advanced_debugging_tools|`false`|Enables sensitive debugging tools that c
384
394
  assets_url|`nil`|See the "Register MiniProfiler's assets in the Rails assets pipeline" section above.
385
395
  snapshot_every_n_requests|`-1`|Determines how frequently snapshots are taken. See the "Snapshots Sampling" above for more details.
386
396
  snapshots_limit|`1000`|Determines how many snapshots Mini Profiler is allowed to keep.
397
+ snapshot_hidden_custom_fields|`[]`|Each snapshot custom field will have a dedicated column in the UI by default. Use this config to exclude certain custom fields from having their own columns.
398
+ snapshots_transport_destination_url|`nil`|Set this config to a valid URL to enable snapshots transporter which will `POST` snapshots to the given URL. The transporter requires `snapshots_transport_auth_key` config to be set as well.
399
+ snapshots_transport_auth_key|`nil`|`POST` requests made by the snapshots transporter to the destination URL will have a `Mini-Profiler-Transport-Auth` header with the value of this config. Make sure you use a secure and random key for this config.
400
+ snapshots_redact_sql_queries|`true`|When this is true, SQL queries will be redacted from sampling snapshots, but the backtrace and duration of each SQL query will be saved with the snapshot to keep debugging performance issues possible.
387
401
 
388
402
  ### Using MiniProfiler with `Rack::Deflate` middleware
389
403
 
@@ -434,7 +434,9 @@
434
434
  background: #6a737c;
435
435
  color: #ffffff; }
436
436
  .mp-snapshots .snapshots-table th, .mp-snapshots .snapshots-table td {
437
- padding: 5px;
437
+ padding: 5px 10px;
438
438
  box-sizing: border-box; }
439
+ .mp-snapshots .snapshots-table th:not(.request-group), .mp-snapshots .snapshots-table td:not(.request-group) {
440
+ text-align: center; }
439
441
  .mp-snapshots .snapshots-table th {
440
442
  border-right: 1px solid #ffffff; }
@@ -679,7 +679,20 @@ var MiniProfiler = (function() {
679
679
  if (data.page === "overview") {
680
680
  temp.innerHTML = MiniProfiler.templates.snapshotsGroupsList(data);
681
681
  } else if (data.group_name) {
682
- temp.innerHTML = MiniProfiler.templates.snapshotsList(data);
682
+ var allCustomFieldsNames = [];
683
+ data.list.forEach(function (snapshot) {
684
+ Object.keys(snapshot.custom_fields).forEach(function (k) {
685
+ if (allCustomFieldsNames.indexOf(k) === -1 &&
686
+ options.hiddenCustomFields.indexOf(k.toLowerCase()) === -1) {
687
+ allCustomFieldsNames.push(k);
688
+ }
689
+ });
690
+ });
691
+ allCustomFieldsNames.sort();
692
+ temp.innerHTML = MiniProfiler.templates.snapshotsList({
693
+ data: data,
694
+ allCustomFieldsNames: allCustomFieldsNames
695
+ });
683
696
  }
684
697
  Array.from(temp.children).forEach(function (child) {
685
698
  document.body.appendChild(child);
@@ -994,6 +1007,7 @@ var MiniProfiler = (function() {
994
1007
  sessionStorage["rack-mini-profiler-start-hidden"] === "true";
995
1008
  var htmlContainer = script.getAttribute("data-html-container");
996
1009
  var cssUrl = script.getAttribute("data-css-url");
1010
+ var hiddenCustomFields = script.getAttribute("data-hidden-custom-fields").toLowerCase().split(",");
997
1011
  return {
998
1012
  ids: ids,
999
1013
  path: path,
@@ -1011,7 +1025,8 @@ var MiniProfiler = (function() {
1011
1025
  startHidden: startHidden,
1012
1026
  collapseResults: collapseResults,
1013
1027
  htmlContainer: htmlContainer,
1014
- cssUrl: cssUrl
1028
+ cssUrl: cssUrl,
1029
+ hiddenCustomFields: hiddenCustomFields
1015
1030
  };
1016
1031
  })();
1017
1032
 
@@ -637,8 +637,11 @@ $zindex: 2147483640; // near 32bit max 2147483647
637
637
  color: #ffffff;
638
638
  }
639
639
  th, td {
640
- padding: 5px;
640
+ padding: 5px 10px;
641
641
  box-sizing: border-box;
642
+ &:not(.request-group) {
643
+ text-align: center;
644
+ }
642
645
  }
643
646
  th {
644
647
  border-right: 1px solid #ffffff;
@@ -17,7 +17,7 @@
17
17
  <span class="profiler-name">
18
18
  {{= it.name}} <span class="profiler-overall-duration">({{= MiniProfiler.formatDuration(it.duration_milliseconds)}} ms)</span>
19
19
  </span>
20
- <span class="profiler-server-time">{{= it.machine_name}} on {{= MiniProfiler.renderDate(it.started)}}</span>
20
+ <span class="profiler-server-time">{{= it.machine_name}} on {{= MiniProfiler.renderDate(it.started_formatted)}}</span>
21
21
  </div>
22
22
  <div class="profiler-output">
23
23
  <table class="profiler-timings">
@@ -217,7 +217,11 @@
217
217
  <td>
218
218
  <div class="query">
219
219
  <pre class="profiler-stack-trace">{{= it.s.stack_trace_snippet }}</pre>
220
- <pre class="prettyprint lang-sql"><code>{{= it.s.formatted_command_string }}; {{= MiniProfiler.formatParameters(it.s.parameters) }}</code></pre>
220
+ {{? it.s.formatted_command_string}}
221
+ <pre class="prettyprint lang-sql"><code>{{= it.s.formatted_command_string }}; {{= MiniProfiler.formatParameters(it.s.parameters) }}</code></pre>
222
+ {{??}}
223
+ <i>Query redacted</i>
224
+ {{?}}
221
225
  </div>
222
226
  </td>
223
227
  </tr>
@@ -241,13 +245,17 @@
241
245
  <tr>
242
246
  <th>Requests Group</th>
243
247
  <th>Worst Time (ms)</th>
248
+ <th>Best Time (ms)</th>
249
+ <th>No. of Snapshots</th>
244
250
  </tr>
245
251
  </thead>
246
252
  <tbody>
247
253
  {{~ it.list :row}}
248
254
  <tr>
249
- <td><a href="{{= row.url }}">{{= row.name }}</a></td>
255
+ <td class="request-group"><a href="{{= row.url }}">{{= row.name }}</a></td>
250
256
  <td>{{= MiniProfiler.formatDuration(row.worst_score) }}</td>
257
+ <td>{{= MiniProfiler.formatDuration(row.best_score) }}</td>
258
+ <td>{{= row.snapshots_count }}</td>
251
259
  </tr>
252
260
  {{~}}
253
261
  </tbody>
@@ -258,23 +266,33 @@
258
266
  </script>
259
267
 
260
268
  <script id="snapshotsList" type="text/x-dot-tmpl">
261
- {{? it.list && it.list.length }}
262
- <h2>Snapshots for {{= it.group_name }}</h2>
269
+ {{ var data = it.data; }}
270
+ {{ var customFieldsNames = it.allCustomFieldsNames; }}
271
+ {{? data.list && data.list.length }}
272
+ <h2>Snapshots for {{= data.group_name }}</h2>
263
273
  <table class="snapshots-table">
264
274
  <thead>
265
275
  <tr>
266
276
  <th>ID</th>
267
277
  <th>Duration (ms)</th>
278
+ <th>SQL Count</th>
279
+ {{~ customFieldsNames :name }}
280
+ <th>{{= name }}</th>
281
+ {{~}}
268
282
  <th>Age</th>
269
283
  </tr>
270
284
  </thead>
271
285
  <tbody>
272
- {{~ it.list :row}}
286
+ {{~ data.list :row}}
273
287
  <tr>
274
288
  <td><a href="{{= row.url }}">
275
289
  {{= row.id }}
276
290
  </a></td>
277
291
  <td>{{= MiniProfiler.formatDuration(row.duration) }}</td>
292
+ <td>{{= row.sql_count }}</td>
293
+ {{~ customFieldsNames :name }}
294
+ <td>{{= row.custom_fields[name] || "" }}</td>
295
+ {{~}}
278
296
  <td>
279
297
  {{? row.timestamp }}
280
298
  {{= MiniProfiler.timestampToRelative(row.timestamp) }}
@@ -285,6 +303,6 @@
285
303
  </tbody>
286
304
  </table>
287
305
  {{??}}
288
- <h2>No snapshots for {{= it.group_name }}</h2>
306
+ <h2>No snapshots for {{= data.group_name }}</h2>
289
307
  {{?}}
290
308
  </script>
@@ -1 +1 @@
1
- <script async type="text/javascript" id="mini-profiler" src="{url}" data-css-url="{cssUrl}" data-version="{version}" data-path="{path}" data-current-id="{currentId}" data-ids="{ids}" data-horizontal-position="{horizontalPosition}" data-vertical-position="{verticalPosition}" data-trivial="{showTrivial}" data-children="{showChildren}" data-max-traces="{maxTracesToShow}" data-controls="{showControls}" data-total-sql-count="{showTotalSqlCount}" data-authorized="{authorized}" data-toggle-shortcut="{toggleShortcut}" data-start-hidden="{startHidden}" data-collapse-results="{collapseResults}" data-html-container="{htmlContainer}"></script>
1
+ <script async type="text/javascript" id="mini-profiler" src="{url}" data-css-url="{cssUrl}" data-version="{version}" data-path="{path}" data-current-id="{currentId}" data-ids="{ids}" data-horizontal-position="{horizontalPosition}" data-vertical-position="{verticalPosition}" data-trivial="{showTrivial}" data-children="{showChildren}" data-max-traces="{maxTracesToShow}" data-controls="{showControls}" data-total-sql-count="{showTotalSqlCount}" data-authorized="{authorized}" data-toggle-shortcut="{toggleShortcut}" data-start-hidden="{startHidden}" data-collapse-results="{collapseResults}" data-html-container="{htmlContainer}" data-hidden-custom-fields="{hiddenCustomFields}"></script>
@@ -7,7 +7,7 @@
7
7
  MiniProfiler.templates = {};
8
8
  MiniProfiler.templates["profilerTemplate"] = function anonymous(it
9
9
  ) {
10
- var out=' <div class="profiler-result"> <div class="profiler-button ';if(it.has_duplicate_sql_timings){out+='profiler-warning';}out+='"> ';if(it.has_duplicate_sql_timings){out+='<span class="profiler-nuclear">!</span>';}out+=' <span class="profiler-number"> '+( MiniProfiler.formatDuration(it.duration_milliseconds))+' <span class="profiler-unit">ms</span> </span> ';if(MiniProfiler.showTotalSqlCount()){out+=' <span class="profiler-number"> '+( it.sql_count)+' <span class="profiler-unit">sql</span> </span> ';}out+=' </div> <div class="profiler-popup"> <div class="profiler-info"> <span class="profiler-name"> '+( it.name)+' <span class="profiler-overall-duration">('+( MiniProfiler.formatDuration(it.duration_milliseconds))+' ms)</span> </span> <span class="profiler-server-time">'+( it.machine_name)+' on '+( MiniProfiler.renderDate(it.started))+'</span> </div> <div class="profiler-output"> <table class="profiler-timings"> <thead> <tr> <th></th> <th>duration (ms)</th> <th class="profiler-duration-with-children">with children (ms)</th> <th class="time-from-start">from start (ms)</th> ';if(it.has_sql_timings){out+=' <th colspan="2">query time (ms)</th> ';}out+=' ';var arr1=it.custom_timing_names;if(arr1){var value,i1=-1,l1=arr1.length-1;while(i1<l1){value=arr1[i1+=1];out+=' <th colspan="2">'+( value.toLowerCase() )+' (ms)</th> ';} } out+=' </tr> </thead> <tbody> '+( MiniProfiler.templates.timingTemplate({timing: it.root, page: it}) )+' </tbody> <tfoot> <tr> <td colspan="3"> ';if(!it.client_timings){out+=' '+( MiniProfiler.templates.linksTemplate({timing: it.root, page: it}) )+' ';}out+=' <a class="profiler-toggle-duration-with-children" title="toggles column with aggregate child durations">show time with children</a> <a class="profiler-snapshots-page-link" title="Go to snapshots page" href="'+( MiniProfiler.options.path )+'snapshots">snapshots</a> </td> ';if(it.has_sql_timings){out+=' <td colspan="2" class="profiler-number profiler-percent-in-sql" title="'+( MiniProfiler.getSqlTimingsCount(it.root) )+' queries spent '+( MiniProfiler.formatDuration(it.duration_milliseconds_in_sql) )+' ms of total request time"> '+( MiniProfiler.formatDuration(it.duration_milliseconds_in_sql / it.duration_milliseconds * 100) )+' <span class="profiler-unit">% in sql</span> </td> ';}out+=' ';var arr2=it.custom_timing_names;if(arr2){var value,i2=-1,l2=arr2.length-1;while(i2<l2){value=arr2[i2+=1];out+=' <td colspan="2" class="profiler-number profiler-percentage-in-sql" title="'+( it.custom_timing_stats[value].count )+' '+( value.toLowerCase() )+' invocations spent '+( MiniProfiler.formatDuration(it.custom_timing_stats[value].duration) )+' ms of total request time"> '+( MiniProfiler.formatDuration(it.custom_timing_stats[value].duration / it.duration_milliseconds * 100) )+' <span class="profiler-unit">% in '+( value.toLowerCase() )+'</span> </td> ';} } out+=' </tr> </tfoot> </table> ';if(it.client_timings){out+=' <table class="profiler-timings profiler-client-timings"> <thead> <tr> <th>client event</th> <th>duration (ms)</th> <th>from start (ms)</th> </tr> </thead> <tbody> ';var arr3=MiniProfiler.getClientTimings(it.client_timings);if(arr3){var value,i3=-1,l3=arr3.length-1;while(i3<l3){value=arr3[i3+=1];out+=' <tr class="';if(value.isTrivial){out+='profiler-trivial';}out+='"> <td class="profiler-label">'+( value.name )+'</td> <td class="profiler-duration"> ';if(value.duration >= 0){out+=' <span class="profiler-unit"></span>'+( MiniProfiler.formatDuration(value.duration) )+' ';}out+=' </td> <td class="profiler-duration time-from-start"> <span class="profiler-unit">+</span>'+( MiniProfiler.formatDuration(value.start) )+' </td> </tr> ';} } out+=' </tbody> <tfoot> <td colspan="3"> '+( MiniProfiler.templates.linksTemplate({timing: it.root, page: it}) )+' </td> </tfoot> </table> ';}out+=' ';if(it.custom_fields && Object.keys(it.custom_fields).length > 0){out+=' <p class="custom-fields-title">Snapshot custom fields</p> <table class="profiler-timings"> <tbody> ';var arr4=Object.keys(it.custom_fields);if(arr4){var key,i4=-1,l4=arr4.length-1;while(i4<l4){key=arr4[i4+=1];out+=' <tr> <td class="profiler-label">'+( key )+'</td> <td class="profiler-label">'+( it.custom_fields[key] )+'</td> </tr> ';} } out+=' </tbody> </table> ';}out+=' </div> </div> ';if(it.has_sql_timings){out+=' <div class="profiler-queries"> <table> <thead> <tr> <th style="text-align:right">step<br />time from start<br />query type<br />duration</th> <th style="text-align:left">call stack<br />query</th> </tr> </thead> <tbody> ';var arr5=MiniProfiler.getSqlTimings(it.root);if(arr5){var value,index=-1,l5=arr5.length-1;while(index<l5){value=arr5[index+=1];out+=' '+( MiniProfiler.templates.sqlGapTemplate({g: value.prevGap}) )+' '+( MiniProfiler.templates.sqlTimingTemplate({i: index, s: value}) )+' ';if(value.nextGap){out+=' '+( MiniProfiler.templates.sqlGapTemplate({g: value.nextGap}) )+' ';}out+=' ';} } out+=' </tbody> </table> <p class="profiler-trivial-gap-container"> <a class="profiler-toggle-trivial-gaps">show trivial gaps</a> </p> </div> ';}out+=' </div>';return out;
10
+ var out=' <div class="profiler-result"> <div class="profiler-button ';if(it.has_duplicate_sql_timings){out+='profiler-warning';}out+='"> ';if(it.has_duplicate_sql_timings){out+='<span class="profiler-nuclear">!</span>';}out+=' <span class="profiler-number"> '+( MiniProfiler.formatDuration(it.duration_milliseconds))+' <span class="profiler-unit">ms</span> </span> ';if(MiniProfiler.showTotalSqlCount()){out+=' <span class="profiler-number"> '+( it.sql_count)+' <span class="profiler-unit">sql</span> </span> ';}out+=' </div> <div class="profiler-popup"> <div class="profiler-info"> <span class="profiler-name"> '+( it.name)+' <span class="profiler-overall-duration">('+( MiniProfiler.formatDuration(it.duration_milliseconds))+' ms)</span> </span> <span class="profiler-server-time">'+( it.machine_name)+' on '+( MiniProfiler.renderDate(it.started_formatted))+'</span> </div> <div class="profiler-output"> <table class="profiler-timings"> <thead> <tr> <th></th> <th>duration (ms)</th> <th class="profiler-duration-with-children">with children (ms)</th> <th class="time-from-start">from start (ms)</th> ';if(it.has_sql_timings){out+=' <th colspan="2">query time (ms)</th> ';}out+=' ';var arr1=it.custom_timing_names;if(arr1){var value,i1=-1,l1=arr1.length-1;while(i1<l1){value=arr1[i1+=1];out+=' <th colspan="2">'+( value.toLowerCase() )+' (ms)</th> ';} } out+=' </tr> </thead> <tbody> '+( MiniProfiler.templates.timingTemplate({timing: it.root, page: it}) )+' </tbody> <tfoot> <tr> <td colspan="3"> ';if(!it.client_timings){out+=' '+( MiniProfiler.templates.linksTemplate({timing: it.root, page: it}) )+' ';}out+=' <a class="profiler-toggle-duration-with-children" title="toggles column with aggregate child durations">show time with children</a> <a class="profiler-snapshots-page-link" title="Go to snapshots page" href="'+( MiniProfiler.options.path )+'snapshots">snapshots</a> </td> ';if(it.has_sql_timings){out+=' <td colspan="2" class="profiler-number profiler-percent-in-sql" title="'+( MiniProfiler.getSqlTimingsCount(it.root) )+' queries spent '+( MiniProfiler.formatDuration(it.duration_milliseconds_in_sql) )+' ms of total request time"> '+( MiniProfiler.formatDuration(it.duration_milliseconds_in_sql / it.duration_milliseconds * 100) )+' <span class="profiler-unit">% in sql</span> </td> ';}out+=' ';var arr2=it.custom_timing_names;if(arr2){var value,i2=-1,l2=arr2.length-1;while(i2<l2){value=arr2[i2+=1];out+=' <td colspan="2" class="profiler-number profiler-percentage-in-sql" title="'+( it.custom_timing_stats[value].count )+' '+( value.toLowerCase() )+' invocations spent '+( MiniProfiler.formatDuration(it.custom_timing_stats[value].duration) )+' ms of total request time"> '+( MiniProfiler.formatDuration(it.custom_timing_stats[value].duration / it.duration_milliseconds * 100) )+' <span class="profiler-unit">% in '+( value.toLowerCase() )+'</span> </td> ';} } out+=' </tr> </tfoot> </table> ';if(it.client_timings){out+=' <table class="profiler-timings profiler-client-timings"> <thead> <tr> <th>client event</th> <th>duration (ms)</th> <th>from start (ms)</th> </tr> </thead> <tbody> ';var arr3=MiniProfiler.getClientTimings(it.client_timings);if(arr3){var value,i3=-1,l3=arr3.length-1;while(i3<l3){value=arr3[i3+=1];out+=' <tr class="';if(value.isTrivial){out+='profiler-trivial';}out+='"> <td class="profiler-label">'+( value.name )+'</td> <td class="profiler-duration"> ';if(value.duration >= 0){out+=' <span class="profiler-unit"></span>'+( MiniProfiler.formatDuration(value.duration) )+' ';}out+=' </td> <td class="profiler-duration time-from-start"> <span class="profiler-unit">+</span>'+( MiniProfiler.formatDuration(value.start) )+' </td> </tr> ';} } out+=' </tbody> <tfoot> <td colspan="3"> '+( MiniProfiler.templates.linksTemplate({timing: it.root, page: it}) )+' </td> </tfoot> </table> ';}out+=' ';if(it.custom_fields && Object.keys(it.custom_fields).length > 0){out+=' <p class="custom-fields-title">Snapshot custom fields</p> <table class="profiler-timings"> <tbody> ';var arr4=Object.keys(it.custom_fields);if(arr4){var key,i4=-1,l4=arr4.length-1;while(i4<l4){key=arr4[i4+=1];out+=' <tr> <td class="profiler-label">'+( key )+'</td> <td class="profiler-label">'+( it.custom_fields[key] )+'</td> </tr> ';} } out+=' </tbody> </table> ';}out+=' </div> </div> ';if(it.has_sql_timings){out+=' <div class="profiler-queries"> <table> <thead> <tr> <th style="text-align:right">step<br />time from start<br />query type<br />duration</th> <th style="text-align:left">call stack<br />query</th> </tr> </thead> <tbody> ';var arr5=MiniProfiler.getSqlTimings(it.root);if(arr5){var value,index=-1,l5=arr5.length-1;while(index<l5){value=arr5[index+=1];out+=' '+( MiniProfiler.templates.sqlGapTemplate({g: value.prevGap}) )+' '+( MiniProfiler.templates.sqlTimingTemplate({i: index, s: value}) )+' ';if(value.nextGap){out+=' '+( MiniProfiler.templates.sqlGapTemplate({g: value.nextGap}) )+' ';}out+=' ';} } out+=' </tbody> </table> <p class="profiler-trivial-gap-container"> <a class="profiler-toggle-trivial-gaps">show trivial gaps</a> </p> </div> ';}out+=' </div>';return out;
11
11
  }
12
12
  MiniProfiler.templates["linksTemplate"] = function anonymous(it
13
13
  ) {
@@ -19,7 +19,7 @@ var out=' <tr class="';if(it.timing.is_trivial){out+='profiler-trivial';}out+='"
19
19
  }
20
20
  MiniProfiler.templates["sqlTimingTemplate"] = function anonymous(it
21
21
  ) {
22
- var out=' <tr class="'+( it.s.row_class || '' )+'" data-timing-id="'+( it.s.parent_timing_id )+'"> <td class="profiler-info"> <div>'+( it.s.parent_timing_name )+'</div> <div class="profiler-number"><span class="profiler-unit">T+</span>'+( MiniProfiler.formatDuration(it.s.start_milliseconds) )+' <span class="profiler-unit">ms</span></div> <div> ';if(it.s.is_duplicate){out+='<span class="profiler-warning">DUPLICATE</span>';}out+=' '+( MiniProfiler.renderExecuteType(it.s.execute_type) )+' </div> <div title="';if(it.s.execute_type == 3){out+='first result fetched: '+( it.s.first_fetch_duration_milliseconds )+'ms';}out+='">'+( MiniProfiler.formatDuration(it.s.duration_milliseconds) )+' <span class="profiler-unit">ms</span></div> </td> <td> <div class="query"> <pre class="profiler-stack-trace">'+( it.s.stack_trace_snippet )+'</pre> <pre class="prettyprint lang-sql"><code>'+( it.s.formatted_command_string )+'; '+( MiniProfiler.formatParameters(it.s.parameters) )+'</code></pre> </div> </td> </tr>';return out;
22
+ var out=' <tr class="'+( it.s.row_class || '' )+'" data-timing-id="'+( it.s.parent_timing_id )+'"> <td class="profiler-info"> <div>'+( it.s.parent_timing_name )+'</div> <div class="profiler-number"><span class="profiler-unit">T+</span>'+( MiniProfiler.formatDuration(it.s.start_milliseconds) )+' <span class="profiler-unit">ms</span></div> <div> ';if(it.s.is_duplicate){out+='<span class="profiler-warning">DUPLICATE</span>';}out+=' '+( MiniProfiler.renderExecuteType(it.s.execute_type) )+' </div> <div title="';if(it.s.execute_type == 3){out+='first result fetched: '+( it.s.first_fetch_duration_milliseconds )+'ms';}out+='">'+( MiniProfiler.formatDuration(it.s.duration_milliseconds) )+' <span class="profiler-unit">ms</span></div> </td> <td> <div class="query"> <pre class="profiler-stack-trace">'+( it.s.stack_trace_snippet )+'</pre> ';if(it.s.formatted_command_string){out+=' <pre class="prettyprint lang-sql"><code>'+( it.s.formatted_command_string )+'; '+( MiniProfiler.formatParameters(it.s.parameters) )+'</code></pre> ';}else{out+=' <i>Query redacted</i> ';}out+=' </div> </td> </tr>';return out;
23
23
  }
24
24
  MiniProfiler.templates["sqlGapTemplate"] = function anonymous(it
25
25
  ) {
@@ -27,11 +27,11 @@ var out=' <tr class="profiler-gap-info';if(it.g.duration < 4){out+=' profiler-tr
27
27
  }
28
28
  MiniProfiler.templates["snapshotsGroupsList"] = function anonymous(it
29
29
  ) {
30
- var out=' ';if(it.list && it.list.length){out+=' <table class="snapshots-table"> <thead> <tr> <th>Requests Group</th> <th>Worst Time (ms)</th> </tr> </thead> <tbody> ';var arr1=it.list;if(arr1){var row,i1=-1,l1=arr1.length-1;while(i1<l1){row=arr1[i1+=1];out+=' <tr> <td><a href="'+( row.url )+'">'+( row.name )+'</a></td> <td>'+( MiniProfiler.formatDuration(row.worst_score) )+'</td> </tr> ';} } out+=' </tbody> </table> ';}else{out+=' <h2>No snapshots exist</h2> ';}return out;
30
+ var out=' ';if(it.list && it.list.length){out+=' <table class="snapshots-table"> <thead> <tr> <th>Requests Group</th> <th>Worst Time (ms)</th> <th>Best Time (ms)</th> <th>No. of Snapshots</th> </tr> </thead> <tbody> ';var arr1=it.list;if(arr1){var row,i1=-1,l1=arr1.length-1;while(i1<l1){row=arr1[i1+=1];out+=' <tr> <td class="request-group"><a href="'+( row.url )+'">'+( row.name )+'</a></td> <td>'+( MiniProfiler.formatDuration(row.worst_score) )+'</td> <td>'+( MiniProfiler.formatDuration(row.best_score) )+'</td> <td>'+( row.snapshots_count )+'</td> </tr> ';} } out+=' </tbody> </table> ';}else{out+=' <h2>No snapshots exist</h2> ';}return out;
31
31
  }
32
32
  MiniProfiler.templates["snapshotsList"] = function anonymous(it
33
33
  ) {
34
- var out=' ';if(it.list && it.list.length){out+=' <h2>Snapshots for '+( it.group_name )+'</h2> <table class="snapshots-table"> <thead> <tr> <th>ID</th> <th>Duration (ms)</th> <th>Age</th> </tr> </thead> <tbody> ';var arr1=it.list;if(arr1){var row,i1=-1,l1=arr1.length-1;while(i1<l1){row=arr1[i1+=1];out+=' <tr> <td><a href="'+( row.url )+'"> '+( row.id )+' </a></td> <td>'+( MiniProfiler.formatDuration(row.duration) )+'</td> <td> ';if(row.timestamp){out+=' '+( MiniProfiler.timestampToRelative(row.timestamp) )+' ';}out+=' </td> </tr> ';} } out+=' </tbody> </table> ';}else{out+=' <h2>No snapshots for '+( it.group_name )+'</h2> ';}return out;
34
+ var out=' '; var data = it.data; out+=' '; var customFieldsNames = it.allCustomFieldsNames; out+=' ';if(data.list && data.list.length){out+=' <h2>Snapshots for '+( data.group_name )+'</h2> <table class="snapshots-table"> <thead> <tr> <th>ID</th> <th>Duration (ms)</th> <th>SQL Count</th> ';var arr1=customFieldsNames;if(arr1){var name,i1=-1,l1=arr1.length-1;while(i1<l1){name=arr1[i1+=1];out+=' <th>'+( name )+'</th> ';} } out+=' <th>Age</th> </tr> </thead> <tbody> ';var arr2=data.list;if(arr2){var row,i2=-1,l2=arr2.length-1;while(i2<l2){row=arr2[i2+=1];out+=' <tr> <td><a href="'+( row.url )+'"> '+( row.id )+' </a></td> <td>'+( MiniProfiler.formatDuration(row.duration) )+'</td> <td>'+( row.sql_count )+'</td> ';var arr3=customFieldsNames;if(arr3){var name,i3=-1,l3=arr3.length-1;while(i3<l3){name=arr3[i3+=1];out+=' <td>'+( row.custom_fields[name] || "" )+'</td> ';} } out+=' <td> ';if(row.timestamp){out+=' '+( MiniProfiler.timestampToRelative(row.timestamp) )+' ';}out+=' </td> </tr> ';} } out+=' </tbody> </table> ';}else{out+=' <h2>No snapshots for '+( data.group_name )+'</h2> ';}return out;
35
35
  }
36
36
 
37
37
  if (typeof prettyPrint === "undefined") {
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  module Rack
3
3
  class MiniProfiler
4
- ASSET_VERSION = 'b5b2bc8cce501b6f38c294cea2f0d2c2'
4
+ ASSET_VERSION = 'f8924c1fd3fbe1787f0bed5c8998b636'
5
5
  end
6
6
  end
@@ -76,6 +76,7 @@ module Rack
76
76
  settings_string = settings.map { |k, v| "#{k}=#{v}" }.join(",")
77
77
  cookie = { value: settings_string, path: '/', httponly: true }
78
78
  cookie[:secure] = true if @request.ssl?
79
+ cookie[:same_site] = 'Lax'
79
80
  Rack::Utils.set_cookie_header!(headers, COOKIE_NAME, cookie)
80
81
  end
81
82
  end
@@ -52,6 +52,10 @@ module Rack
52
52
  @toggle_shortcut = 'alt+p'
53
53
  @html_container = 'body'
54
54
  @position = "top-left"
55
+ @snapshot_hidden_custom_fields = []
56
+ @snapshots_transport_destination_url = nil
57
+ @snapshots_transport_auth_key = nil
58
+ @snapshots_redact_sql_queries = true
55
59
 
56
60
  self
57
61
  }
@@ -63,14 +67,18 @@ module Rack
63
67
  :flamegraph_sample_rate, :logger, :pre_authorize_cb, :skip_paths,
64
68
  :skip_schema_queries, :storage, :storage_failure, :storage_instance,
65
69
  :storage_options, :user_provider, :enable_advanced_debugging_tools,
66
- :snapshot_every_n_requests, :snapshots_limit
67
- attr_accessor :skip_sql_param_names, :suppress_encoding, :max_sql_param_length
70
+ :skip_sql_param_names, :suppress_encoding, :max_sql_param_length
68
71
 
69
72
  # ui accessors
70
73
  attr_accessor :collapse_results, :max_traces_to_show, :position,
71
74
  :show_children, :show_controls, :show_trivial, :show_total_sql_count,
72
75
  :start_hidden, :toggle_shortcut, :html_container
73
76
 
77
+ # snapshot related config
78
+ attr_accessor :snapshot_every_n_requests, :snapshots_limit,
79
+ :snapshot_hidden_custom_fields, :snapshots_transport_destination_url,
80
+ :snapshots_transport_auth_key, :snapshots_redact_sql_queries
81
+
74
82
  # Deprecated options
75
83
  attr_accessor :use_existing_jquery
76
84
 
@@ -39,6 +39,7 @@ module Rack
39
39
  def current=(c)
40
40
  # we use TLS cause we need access to this from sql blocks and code blocks that have no access to env
41
41
  Thread.current[:mini_profiler_snapshot_custom_fields] = nil
42
+ Thread.current[:mp_ongoing_snapshot] = nil
42
43
  Thread.current[:mini_profiler_private] = c
43
44
  end
44
45
 
@@ -94,6 +95,16 @@ module Rack
94
95
  params
95
96
  end
96
97
  end
98
+
99
+ def snapshots_transporter?
100
+ !!config.snapshots_transport_destination_url &&
101
+ !!config.snapshots_transport_auth_key
102
+ end
103
+
104
+ def redact_sql_queries?
105
+ Thread.current[:mp_ongoing_snapshot] == true &&
106
+ Rack::MiniProfiler.config.snapshots_redact_sql_queries
107
+ end
97
108
  end
98
109
 
99
110
  #
@@ -689,7 +700,8 @@ Append the following to your query string:
689
700
  toggleShortcut: @config.toggle_shortcut,
690
701
  startHidden: @config.start_hidden,
691
702
  collapseResults: @config.collapse_results,
692
- htmlContainer: @config.html_container
703
+ htmlContainer: @config.html_container,
704
+ hiddenCustomFields: @config.snapshot_hidden_custom_fields.join(',')
693
705
  }
694
706
 
695
707
  if current && current.page_struct
@@ -799,6 +811,7 @@ Append the following to your query string:
799
811
 
800
812
  def take_snapshot(env, start)
801
813
  MiniProfiler.create_current(env, @config)
814
+ Thread.current[:mp_ongoing_snapshot] = true
802
815
  results = @app.call(env)
803
816
  status = results[0].to_i
804
817
  if status >= 200 && status < 300
@@ -808,10 +821,14 @@ Append the following to your query string:
808
821
  )
809
822
  custom_fields = MiniProfiler.get_snapshot_custom_fields
810
823
  page_struct[:custom_fields] = custom_fields if custom_fields
811
- @storage.push_snapshot(
812
- page_struct,
813
- @config
814
- )
824
+ if Rack::MiniProfiler.snapshots_transporter?
825
+ Rack::MiniProfiler::SnapshotsTransporter.transport(page_struct)
826
+ else
827
+ @storage.push_snapshot(
828
+ page_struct,
829
+ @config
830
+ )
831
+ end
815
832
  end
816
833
  self.current = nil
817
834
  results
@@ -7,7 +7,14 @@ module Rack
7
7
  def record_sql(query, elapsed_ms, params = nil)
8
8
  return unless current && current.current_timer
9
9
  c = current
10
- c.current_timer.add_sql(query, elapsed_ms, c.page_struct, params, c.skip_backtrace, c.full_backtrace)
10
+ c.current_timer.add_sql(
11
+ redact_sql_queries? ? nil : query,
12
+ elapsed_ms,
13
+ c.page_struct,
14
+ redact_sql_queries? ? nil : params,
15
+ c.skip_backtrace,
16
+ c.full_backtrace
17
+ )
11
18
  end
12
19
 
13
20
  def start_step(name)
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ::Rack::MiniProfiler::SnapshotsTransporter
4
+ @@transported_snapshots_count = 0
5
+ @@successful_http_requests_count = 0
6
+ @@failed_http_requests_count = 0
7
+
8
+ class << self
9
+ def transported_snapshots_count
10
+ @@transported_snapshots_count
11
+ end
12
+ def successful_http_requests_count
13
+ @@successful_http_requests_count
14
+ end
15
+ def failed_http_requests_count
16
+ @@failed_http_requests_count
17
+ end
18
+
19
+ def transport(snapshot)
20
+ @transporter ||= self.new(Rack::MiniProfiler.config)
21
+ @transporter.ship(snapshot)
22
+ end
23
+ end
24
+
25
+ attr_reader :buffer
26
+ attr_accessor :max_buffer_size
27
+
28
+ def initialize(config)
29
+ @uri = URI(config.snapshots_transport_destination_url)
30
+ @auth_key = config.snapshots_transport_auth_key
31
+ @thread = nil
32
+ @thread_mutex = Mutex.new
33
+ @buffer = []
34
+ @buffer_mutex = Mutex.new
35
+ @max_buffer_size = 100
36
+ @testing = false
37
+ end
38
+
39
+ def ship(snapshot)
40
+ @buffer_mutex.synchronize do
41
+ @buffer << snapshot
42
+ @buffer.shift if @buffer.size > @max_buffer_size
43
+ end
44
+ @thread_mutex.synchronize { start_thread }
45
+ end
46
+
47
+ def flush_buffer
48
+ buffer_content = @buffer_mutex.synchronize do
49
+ @buffer.dup if @buffer.size > 0
50
+ end
51
+ if buffer_content
52
+ request = Net::HTTP::Post.new(
53
+ @uri,
54
+ 'Content-Type' => 'application/json',
55
+ 'Mini-Profiler-Transport-Auth' => @auth_key
56
+ )
57
+ request.body = { snapshots: buffer_content }.to_json
58
+ http = Net::HTTP.new(@uri.hostname, @uri.port)
59
+ http.use_ssl = @uri.scheme == 'https'
60
+ res = http.request(request)
61
+ if res.code.to_i == 200
62
+ @@successful_http_requests_count += 1
63
+ @@transported_snapshots_count += buffer_content.size
64
+ @buffer_mutex.synchronize do
65
+ @buffer -= buffer_content
66
+ end
67
+ else
68
+ @@failed_http_requests_count += 1
69
+ end
70
+ end
71
+ end
72
+
73
+ private
74
+
75
+ def start_thread
76
+ return if @thread&.alive? || @testing
77
+ @thread = Thread.new do
78
+ while true
79
+ sleep 10
80
+ flush_buffer
81
+ end
82
+ end
83
+ end
84
+ end
@@ -58,17 +58,21 @@ module Rack
58
58
  fetch_snapshots do |batch|
59
59
  batch.each do |snapshot|
60
60
  group_name = default_snapshot_grouping(snapshot)
61
- if !groups[group_name] || groups[group_name] < snapshot.duration_ms
62
- groups[group_name] = snapshot.duration_ms
61
+ hash = groups[group_name] ||= {}
62
+ hash[:snapshots_count] ||= 0
63
+ hash[:snapshots_count] += 1
64
+ if !hash[:worst_score] || hash[:worst_score] < snapshot.duration_ms
65
+ groups[group_name][:worst_score] = snapshot.duration_ms
66
+ end
67
+ if !hash[:best_score] || hash[:best_score] > snapshot.duration_ms
68
+ groups[group_name][:best_score] = snapshot.duration_ms
63
69
  end
64
70
  end
65
71
  end
66
72
  groups = groups.to_a
67
- groups.sort_by! { |name, score| score }
73
+ groups.sort_by! { |name, hash| hash[:worst_score] }
68
74
  groups.reverse!
69
- groups.map! do |name, score|
70
- { name: name, worst_score: score }
71
- end
75
+ groups.map! { |name, hash| hash.merge(name: name) }
72
76
  groups
73
77
  end
74
78
 
@@ -81,7 +85,9 @@ module Rack
81
85
  data << {
82
86
  id: snapshot[:id],
83
87
  duration: snapshot.duration_ms,
84
- timestamp: snapshot[:started_at]
88
+ sql_count: snapshot[:sql_count],
89
+ timestamp: snapshot[:started_at],
90
+ custom_fields: snapshot[:custom_fields]
85
91
  }
86
92
  end
87
93
  end
@@ -10,6 +10,53 @@ module Rack
10
10
  # :has_many TimerStruct::Sql children
11
11
  # :has_many TimerStruct::Custom children
12
12
  class Page < TimerStruct::Base
13
+ class << self
14
+ def from_hash(hash)
15
+ hash = symbolize_hash(hash)
16
+ if hash.key?(:custom_timing_names)
17
+ hash[:custom_timing_names] = []
18
+ end
19
+ hash.delete(:started_formatted)
20
+ if hash.key?(:duration_milliseconds)
21
+ hash[:duration_milliseconds] = 0
22
+ end
23
+ page = self.allocate
24
+ page.instance_variable_set(:@attributes, hash)
25
+ page
26
+ end
27
+
28
+ private
29
+
30
+ def symbolize_hash(hash)
31
+ new_hash = {}
32
+ hash.each do |k, v|
33
+ sym_k = String === k ? k.to_sym : k
34
+ if Hash === v
35
+ new_hash[sym_k] = symbolize_hash(v)
36
+ elsif Array === v
37
+ new_hash[sym_k] = symbolize_array(v)
38
+ else
39
+ new_hash[sym_k] = v
40
+ end
41
+ end
42
+ new_hash
43
+ end
44
+
45
+ def symbolize_array(array)
46
+ array.map do |item|
47
+ if Array === item
48
+ symbolize_array(item)
49
+ elsif Hash === item
50
+ symbolize_hash(item)
51
+ else
52
+ item
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ attr_reader :attributes
59
+
13
60
  def initialize(env)
14
61
  timer_id = MiniProfiler.generate_id
15
62
  page_name = env['PATH_INFO']
@@ -74,7 +121,7 @@ module Rack
74
121
 
75
122
  def extra_json
76
123
  {
77
- started: '/Date(%d)/' % @attributes[:started_at],
124
+ started_formatted: '/Date(%d)/' % @attributes[:started_at],
78
125
  duration_milliseconds: @attributes[:root][:duration_milliseconds],
79
126
  custom_timing_names: @attributes[:custom_timing_stats].keys.sort
80
127
  }
@@ -38,12 +38,12 @@ module Rack
38
38
  start_millis = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) * 1000).to_i - page[:started]) - duration_ms
39
39
  super(
40
40
  execute_type: 3, # TODO
41
- formatted_command_string: ERB::Util.html_escape(query),
41
+ formatted_command_string: query ? ERB::Util.html_escape(query) : nil,
42
42
  stack_trace_snippet: stack_trace,
43
43
  start_milliseconds: start_millis,
44
44
  duration_milliseconds: duration_ms,
45
45
  first_fetch_duration_milliseconds: duration_ms,
46
- parameters: trim_binds(params),
46
+ parameters: query ? trim_binds(params) : nil,
47
47
  parent_timing_id: nil,
48
48
  is_duplicate: false
49
49
  )
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Rack
4
4
  class MiniProfiler
5
- VERSION = '2.1.0'
5
+ VERSION = '2.2.0'
6
6
  end
7
7
  end
@@ -26,6 +26,7 @@ require 'mini_profiler/profiling_methods'
26
26
  require 'mini_profiler/context'
27
27
  require 'mini_profiler/client_settings'
28
28
  require 'mini_profiler/gc_profiler'
29
+ require 'mini_profiler/snapshots_transporter'
29
30
  require 'mini_profiler/profiler'
30
31
  require 'patches/sql_patches'
31
32
  require 'patches/net_patches'
@@ -42,6 +42,7 @@ Gem::Specification.new do |s|
42
42
  s.add_development_dependency 'listen'
43
43
  s.add_development_dependency 'webpacker', '~> 5.1'
44
44
  s.add_development_dependency 'rails', '~> 5.1'
45
+ s.add_development_dependency 'webmock', '3.9.1'
45
46
 
46
47
  s.require_paths = ["lib"]
47
48
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rack-mini-profiler
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.0
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sam Saffron
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2020-09-17 00:00:00.000000000 Z
13
+ date: 2020-10-20 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: rack
@@ -222,6 +222,20 @@ dependencies:
222
222
  - - "~>"
223
223
  - !ruby/object:Gem::Version
224
224
  version: '5.1'
225
+ - !ruby/object:Gem::Dependency
226
+ name: webmock
227
+ requirement: !ruby/object:Gem::Requirement
228
+ requirements:
229
+ - - '='
230
+ - !ruby/object:Gem::Version
231
+ version: 3.9.1
232
+ type: :development
233
+ prerelease: false
234
+ version_requirements: !ruby/object:Gem::Requirement
235
+ requirements:
236
+ - - '='
237
+ - !ruby/object:Gem::Version
238
+ version: 3.9.1
225
239
  description: Profiling toolkit for Rack applications with Rails integration. Client
226
240
  Side profiling, DB profiling and Server profiling.
227
241
  email: sam.saffron@gmail.com
@@ -255,6 +269,7 @@ files:
255
269
  - lib/mini_profiler/gc_profiler.rb
256
270
  - lib/mini_profiler/profiler.rb
257
271
  - lib/mini_profiler/profiling_methods.rb
272
+ - lib/mini_profiler/snapshots_transporter.rb
258
273
  - lib/mini_profiler/storage/abstract_store.rb
259
274
  - lib/mini_profiler/storage/file_store.rb
260
275
  - lib/mini_profiler/storage/memcache_store.rb