10000 feature #28919 [DX][WebProfilerBundle] Add Pretty Print functionality… · symfony/symfony@4e1ad10 · GitHub
[go: up one dir, main page]

Skip to content

Commit 4e1ad10

Browse files
committed
feature #28919 [DX][WebProfilerBundle] Add Pretty Print functionality for Request Content (SamFleming)
This PR was squashed before being merged into the 4.3-dev branch (closes #28919). Discussion ---------- [DX][WebProfilerBundle] Add Pretty Print functionality for Request Content | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | | License | MIT | Doc PR | n/a ? ## Why? Quite often when attempting to debug issues with JSON requests sent to a Symfony API, I use the Web Profiler to check the request content. More often than not the request content isn't easily readable (99% of the time it's all stuck on a single line and impossible to read). I always find myself copying + pasting the content into a random online tool to have it "pretty-print" the JSON. Usually this isn't an issue, but can be annoying when offline. There's also the security issue of sending entire JSON payloads to a third-party server just for formatting 😳. Alternatively, maybe developers copy+paste into their chosen editors and this PR is all a waste of time — I hope not 😛. ## How? This PR adds "Pretty-Print" JSON functionality straight into the profiler. We can use `collector.requestheaders` to detect if the request was JSON and conditionally show the Pretty Print button. When the button is clicked, we format the JSON from the "Request Content" card. ## What does it look like? Before: ![without-pretty-print](https://user-images.githubusercontent.com/573318/47180751-36b0ce00-d319-11e8-86ed-eb0d78ebcbe3.png) After: ![pretty](https://user-images.githubusercontent.com/573318/47180763-3c0e1880-d319-11e8-995d-eba565aad827.png) Non-JSON Requests (unchanged): ![non-json-request](https://user-images.githubusercontent.com/573318/47181080-03227380-d31a-11e8-8cf2-e8b2e8c1a21d.png) ## Things to consider - Is `JSON.stringify(JSON.parse(content));` the safest, most efficient way to do this? - Should the "Pretty Print" button be in-line next to the "Request Content" header? I couldn't find a pattern for this sort of thing elsewhere in the profiler. - Do people want JSON formatted with 4 spaces, would 2 spaces be preferred? Should this be a configuration option stored in localStorage (such as the light/dark theme configuration)? - Should this be a toggle? E.g. click to pretty print, then click to undo ## Future Improvements Depending on how this is received it could be extended to support formatting different request content-types (e.g. XML formatting) — I assume. ## Progress - [x] Gather feedback and decide where to perform the pretty-print: [server-side, or client-side](#28919 (comment)). *It was decided server-side would be better.* Commits ------- 9f85103 [DX][WebProfilerBundle] Add Pretty Print functionality for Request Content
2 parents 93c2feb + 9f85103 commit 4e1ad10

File tree

5 files changed

+91
-6
lines changed

5 files changed

+91
-6
lines changed

src/Symfony/Bundle/TwigBundle/Resources/views/base_js.html.twig

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
4242
/* create the tab navigation for each group of tabs */
4343
for (var i = 0; i < tabGroups.length; i++) {
44-
var tabs = tabGroups[i].querySelectorAll('.tab');
44+
var tabs = tabGroups[i].querySelectorAll(':scope > .tab');
4545
var tabNavigation = document.createElement('ul');
4646
tabNavigation.className = 'tab-navigation';
4747
@@ -67,7 +67,7 @@
6767
6868
/* display the active tab and add the 'click' event listeners */
6969
for (i = 0; i < tabGroups.length; i++) {
70-
tabNavigation = tabGroups[i].querySelectorAll('.tab-navigation li');
70+
tabNavigation = tabGroups[i].querySelectorAll(':scope >.tab-navigation li');
7171
7272
for (j = 0; j < tabNavigation.length; j++) {
7373
tabId = tabNavigation[j].getAttribute('data-tab-id');

src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/request.html.twig

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,8 +161,27 @@
161161
<p>Request content not available (it was retrieved as a resource).</p>
162162
</div>
163163
{% elseif collector.content %}
164-
<div class="card">
165-
<pre class="break-long-words">{{ collector.content }}</pre>
164+
<div class="sf-tabs">
165+
{% set prettyJson = collector.isJsonRequest ? collector.prettyJson : null %}
166+
{% if prettyJson is not null %}
167+
<div class="tab">
168+
<h3 class="tab-title">Pretty</h3>
169+
<div class="tab-content">
170+
<div class="card" style="max-height: 500px; overflow-y: auto;">
171+
<pre class="break-long-words">{{ prettyJson }}</pre>
172+
</div>
173+
</div>
174+
</div>
175+
{% endif %}
176+
177+
<div class="tab">
178+
<h3 class="tab-title">Raw</h3>
179+
<div class="tab-content">
180+
<div class="card">
181+
<pre class="break-long-words">{{ collector.content }}</pre>
182+
</div>
183+
</div>
184+
</div>
166185
</div>
167186
{% else %}
168187
<div class="empty">

src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base_js.html.twig

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -556,7 +556,7 @@
556556
557557
/* create the tab navigation for each group of tabs */
558558
for (var i = 0; i < tabGroups.length; i++) {
559-
var tabs = tabGroups[i].querySelectorAll('.tab');
559+
var tabs = tabGroups[i].querySelectorAll(':scope > .tab');
560560
var tabNavigation = document.createElement('ul');
561561
tabNavigation.className = 'tab-navigation';
562562
@@ -582,7 +582,7 @@
582582
583583
/* display the active tab and add the 'click' event listeners */
584584
for (i = 0; i < tabGroups.length; i++) {
585-
tabNavigation = tabGroups[i].querySelectorAll('.tab-navigation li');
585+
tabNavigation = tabGroups[i].querySelectorAll(':scope > .tab-navigation li');
586586
587587
for (j = 0; j < tabNavigation.length; j++) {
588588
tabId = tabNavigation[j].getAttribute('data-tab-id');

src/Symfony/Component/HttpKernel/DataCollector/RequestDataCollector.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,18 @@ public function getContent()
252252
return $this->data['content'];
253253
}
254254

255+
public function isJsonRequest()
256+
{
257+
return 1 === preg_match('{^application/(?:\w+\++)*json$}i', $this->data['request_headers']['content-type']);
258+
}
259+
260+
public function getPrettyJson()
261+
{
262+
$decoded = json_decode($this->getContent());
263+
264+
return JSON_ERROR_NONE === json_last_error() ? json_encode($decoded, JSON_PRETTY_PRINT) : null;
265+
}
266+
255267
public function getContentType()
256268
{
257269
return $this->data['content_type'];

src/Symfony/Component/HttpKernel/Tests/DataCollector/RequestDataCollectorTest.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,4 +333,58 @@ private function getCookieByName(Response $response, $name)
333333

334334
throw new \InvalidArgumentException(sprintf('Cookie named "%s" is not in response', $name));
335335
}
336+
337+
/**
338+
* @dataProvider provideJsonContentTypes
339+
*/
340+
public function testIsJson($contentType, $expected)
341+
{
342+
$response = $this->createResponse();
343+
$request = $this->createRequest();
344+
$request->headers->set('Content-Type', $contentType);
345+
346+
$c = new RequestDataCollector();
347+
$c->collect($request, $response);
348+
349+
$this->assertSame($expected, $c->isJsonRequest());
350+
}
351+
352+
public function provideJsonContentTypes()
353+
{
354+
return array(
355+
array('text/csv', false),
356+
array('application/json', true),
357+
array('application/JSON', true),
358+
array('application/hal+json', true),
359+
array('application/xml+json', true),
360+
array('application/xml', false),
361+
array('', false),
362+
);
363+
}
364+
365+
/**
366+
* @dataProvider providePrettyJson
367+
*/
368+
public function testGetPrettyJsonValidity($content, $expected)
369+
{
370+
$response = $this->createResponse();
371+
$request = Request::create('/', 'POST', array(), array(), array(), array(), $content);
372+
373+
$c = new RequestDataCollector();
374+
$c->collect($request, $response);
375+
376+
$this->assertSame($expected, $c->getPrettyJson());
377+
}
378+
379+
public function providePrettyJson()
380+
{
381+
return array(
382+
array('null', 'null'),
383+
array('{ "foo": "bar" }', '{
384+
"foo": "bar"
385+
}'),
386+
array('{ "abc" }', null),
387+
array('', null),
388+
);
389+
}
336390
}

0 commit comments

Comments
 (0)
0