Skip to content

Commit ef33ff6

Browse files
committed
[WebProfilerBundle] Show EventStreams in debug toolbar
1 parent 940731f commit ef33ff6

File tree

4 files changed

+146
-0
lines changed

4 files changed

+146
-0
lines changed

src/Symfony/Bundle/WebProfilerBundle/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
7.4
5+
---
6+
7+
* Add support for Server-Sent Events / `EventStream`s in the debug toolbar
8+
49
7.3
510
---
611

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

Lines changed: 20 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,24 @@ 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+
[$response->headers->get('X-Debug-Token'), $response->headers->get('X-Debug-Token-Link')],
125+
'symfony:debug:started',
126+
));
127+
try {
128+
$callback();
129+
} catch (\Throwable $e) {
130+
$response->sendEvent(new ServerEvent('error', 'symfony:debug:error'));
131+
throw $e;
132+
} finally {
133+
$response->sendEvent(new ServerEvent('-', 'symfony:debug:finished'));
134+
}
135+
});
136+
}
137+
118138
if (self::DISABLED === $this->mode
119139
|| !$response->headers->has('X-Debug-Token')
120140
|| $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
@@ -14,8 +14,10 @@
1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\Bundle\WebProfilerBundle\Csp\ContentSecurityPolicyHandler;
1616
use Symfony\Bundle\WebProfilerBundle\EventListener\WebDebugToolbarListener;
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\HttpKernel\DataCollector\DumpDataCollector;
2022
use Symfony\Component\HttpKernel\Event\ResponseEvent;
2123
use Symfony\Component\HttpKernel\HttpKernelInterface;
@@ -420,6 +422,80 @@ public function testAjaxReplaceHeaderOnEnabledAndXHRButPreviouslySet()
420422
$this->assertSame('0', $response->headers->get('Symfony-Debug-Toolbar-Replace'));
421423
}
422424

425+
public function testEventStreamResponseHasDebugEvents()
426+
{
427+
$request = new Request();
428+
$response = new EventStreamResponse(
429+
fn () => yield new ServerEvent('some data'),
430+
headers: [
431+
'X-Debug-Token' => 'aabbcc',
432+
'X-Debug-Token-Link' => 'test://foobar',
433+
],
434+
);
435+
$event = new ResponseEvent($this->createMock(Kernel::class), $request, HttpKernelInterface::MAIN_REQUEST, $response);
436+
437+
$listener = new WebDebugToolbarListener($this->getTwigMock());
438+
439+
$listener->onKernelResponse($event);
440+
441+
$this->expectOutputString(
442+
<<<'EVENTSTREAM'
443+
event: symfony:debug:started
444+
data: aabbcc
445+
data: test://foobar
446+
447+
data: some data
448+
449+
event: symfony:debug:finished
450+
data: -
451+
452+
453+
EVENTSTREAM
454+
);
455+
$response->send(false);
456+
}
457+
458+
public function testEventStreamResponseHasDebugEventForException()
459+
{
460+
$request = new Request();
461+
$response = new EventStreamResponse(
462+
function () {
463+
yield new ServerEvent('some data');
464+
throw new \RuntimeException('Something went wrong');
465+
},
466+
headers: [
467+
'X-Debug-Token' => 'aabbcc',
468+
'X-Debug-Token-Link' => 'test://foobar',
469+
],
470+
);
471+
$event = new ResponseEvent($this->createMock(Kernel::class), $request, HttpKernelInterface::MAIN_REQUEST, $response);
472+
473+
$listener = new WebDebugToolbarListener($this->getTwigMock());
474+
475+
$listener->onKernelResponse($event);
476+
477+
$this->expectOutputString(
478+
<<<'EVENTSTREAM'
479+
event: symfony:debug:started
480+
data: aabbcc
481+
data: test://foobar
482+
483+
data: some data
484+
485+
event: symfony:debug:error
486+
data: error
487+
488+
event: symfony:debug:finished
489+
data: -
490+
491+
492+
EVENTSTREAM
493+
);
494+
$this->expectException(\RuntimeException::class);
495+
$this->expectExceptionMessage('Something went wrong');
496+
$response->send(false);
497+
}
498+
423499
protected function getTwigMock($render = 'WDT')
424500
{
425501
$templating = $this->createMock(Environment::class);

0 commit comments

Comments
 (0)