Skip to content

Commit 58dc932

Browse files
committed
[WebProfilerBundle] Show EventStreams in debug toolbar
1 parent 67016b9 commit 58dc932

File tree

4 files changed

+145
-0
lines changed

4 files changed

+145
-0
lines changed

src/Symfony/Bundle/WebProfilerBundle/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
---
66

77
* Add support for the `QUERY` HTTP method in the profiler
8+
* Add support for Server-Sent Events / `EventSource` requests in the debug toolbar
89

910
7.3
1011
---

src/Symfony/Bundle/WebProfilerBundle/EventListener/WebDebugToolbarListener.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@
1414
use Symfony\Bundle\FullStack;
1515
use Symfony\Bundle\WebProfilerBundle\Csp\ContentSecurityPolicyHandler;
1616
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
17+
use Symfony\Component\HttpFoundation\EventStreamResponse;
1718
use Symfony\Component\HttpFoundation\Request;
1819
use Symfony\Component\HttpFoundation\Response;
20+
use Symfony\Component\HttpFoundation\ServerEvent;
1921
use Symfony\Component\HttpFoundation\Session\Flash\AutoExpireFlashBag;
2022
use Symfony\Component\HttpKernel\DataCollector\DumpDataCollector;
2123
use Symfony\Component\HttpKernel\Event\ResponseEvent;
@@ -115,6 +117,27 @@ public function onKernelResponse(ResponseEvent $event): void
115117
$response->headers->remove('Location');
116118
}
117119

120+
if ($response->headers->has('X-Debug-Token') && $response instanceof EventStreamResponse) {
121+
$callback = $response->getCallback();
122+
$response->setCallback(static function () use ($callback, $response) {
123+
$response->sendEvent(new ServerEvent(
124+
[
125+
$response->headers->get('X-Debug-Token') ?? '',
126+
$response->headers->get('X-Debug-Token-Link') ?? '',
127+
],
128+
'symfony:debug:started',
129+
));
130+
try {
131+
$callback();
132+
} catch (\Throwable $e) {
133+
$response->sendEvent(new ServerEvent('error', 'symfony:debug:error'));
134+
throw $e;
135+
} finally {
136+
$response->sendEvent(new ServerEvent('-', 'symfony:debug:finished'));
137+
}
138+
});
139+
}
140+
118141
if (self::DISABLED === $this->mode
119142
|| !$response->headers->has('X-Debug-Token')
120143
|| $response->isRedirection()

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

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,51 @@
295295
};
296296
297297
{% if excluded_ajax_paths is defined %}
298+
if (window.EventSource) {
299+
var oldEventSource = window.EventSource;
300+
window.EventSource = function (url, options) {
301+
var es = new oldEventSource(url, options);
302+
if (!url.match(new RegExp({{ excluded_ajax_paths|json_encode|raw }}))) {
303+
var stackElement = {
304+
error: false,
305+
url: url,
306+
method: 'GET',
307+
type: 'event-stream',
308+
start: new Date()
309+
};
310+
311+
var idx = requestStack.push(stackElement) - 1;
312+
startAjaxRequest(idx);
313+
addEventListener(es, 'error', function () {
314+
stackElement.error = true;
315+
finishAjaxRequest(idx);
316+
});
317+
addEventListener(es, 'open', function () {
318+
stackElement.statusCode = 200;
319+
stackElement.toolbarReplaceFinished = false;
320+
stackElement.toolbarReplace = true;
321+
});
322+
addEventListener(es, 'symfony:debug:started', function (event) {
323+
var items = event.data.split('\n');
324+
stackElement.profile = items[0];
325+
stackElement.profilerUrl = items[1];
326+
});
327+
addEventListener(es, 'symfony:debug:error', function (event) {
328+
stackElement.error = true;
329+
stackElement.statusCode = event.data;
330+
finishAjaxRequest(idx);
331+
});
332+
addEventListener(es, 'symfony:debug:finished', function () {
333+
stackElement.duration = new Date() - stackElement.start;
334+
stackElement.toolbarReplaceFinished = false;
335+
stackElement.toolbarReplace = true;
336+
finishAjaxRequest(idx);
337+
});
338+
}
339+
340+
return es;
341+
};
342+
}
298343
if (window.fetch && window.fetch.polyfill === undefined) {
299344
var oldFetch = window.fetch;
300345
window.fetch = function () {

src/Symfony/Bundle/WebProfilerBundle/Tests/EventListener/WebDebugToolbarListenerTest.php

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@
1616
use PHPUnit\Framework\TestCase;
1717
use Symfony\Bundle\WebProfilerBundle\Csp\ContentSecurityPolicyHandler;
1818
use Symfony\Bundle\WebProfilerBundle\EventListener\WebDebugToolbarListener;
19+
use Symfony\Component\HttpFoundation\EventStreamResponse;
1920
use Symfony\Component\HttpFoundation\Request;
2021
use Symfony\Component\HttpFoundation\Response;
22+
use Symfony\Component\HttpFoundation\ServerEvent;
2123
use Symfony\Component\HttpKernel\DataCollector\DumpDataCollector;
2224
use Symfony\Component\HttpKernel\Event\ResponseEvent;
2325
use Symfony\Component\HttpKernel\HttpKernelInterface;
@@ -401,6 +403,80 @@ public function testAjaxReplaceHeaderOnEnabledAndXHRButPreviouslySet()
401403
$this->assertSame('0', $response->headers->get('Symfony-Debug-Toolbar-Replace'));
402404
}
403405

406+
public function testEventStreamResponseHasDebugEvents()
407+
{
408+
$request = new Request();
409+
$response = new EventStreamResponse(
410+
fn () => yield new ServerEvent('some data'),
411+
headers: [
412+
'X-Debug-Token' => 'aabbcc',
413+
'X-Debug-Token-Link' => 'test://foobar',
414+
],
415+
);
416+
$event = new ResponseEvent($this->createMock(Kernel::class), $request, HttpKernelInterface::MAIN_REQUEST, $response);
417+
418+
$listener = new WebDebugToolbarListener($this->getTwigMock());
419+
420+
$listener->onKernelResponse($event);
421+
422+
$this->expectOutputString(
423+
<<<'EVENTSTREAM'
424+
event: symfony:debug:started
425+
data: aabbcc
426+
data: test://foobar
427+
428+
data: some data
429+
430+
event: symfony:debug:finished
431+
data: -
432+
433+
434+
EVENTSTREAM
435+
);
436+
$response->send(false);
437+
}
438+
439+
public function testEventStreamResponseHasDebugEventForException()
440+
{
441+
$request = new Request();
442+
$response = new EventStreamResponse(
443+
function () {
444+
yield new ServerEvent('some data');
445+
throw new \RuntimeException('Something went wrong');
446+
},
447+
headers: [
448+
'X-Debug-Token' => 'aabbcc',
449+
'X-Debug-Token-Link' => 'test://foobar',
450+
],
451+
);
452+
$event = new ResponseEvent($this->createMock(Kernel::class), $request, HttpKernelInterface::MAIN_REQUEST, $response);
453+
454+
$listener = new WebDebugToolbarListener($this->getTwigMock());
455+
456+
$listener->onKernelResponse($event);
457+
458+
$this->expectOutputString(
459+
<<<'EVENTSTREAM'
460+
event: symfony:debug:started
461+
data: aabbcc
462+
data: test://foobar
463+
464+
data: some data
465+
466+
event: symfony:debug:error
467+
data: error
468+
469+
event: symfony:debug:finished
470+
data: -
471+
472+
473+
EVENTSTREAM
474+
);
475+
$this->expectException(\RuntimeException::class);
476+
$this->expectExceptionMessage('Something went wrong');
477+
$response->send(false);
478+
}
479+
404480
protected function getTwigMock($render = 'WDT')
405481
{
406482
$templating = $this->createMock(Environment::class);

0 commit comments

Comments
 (0)