Skip to content

Commit 6d29db8

Browse files
committed
Implementation of optional sentry tracing with twig
1 parent d526d9d commit 6d29db8

File tree

17 files changed

+271
-2
lines changed

17 files changed

+271
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## Unreleased
44

5+
- Add support for distributed tracing of Twig template rendering (#430)
56
- Add support for distributed tracing of SQL queries while using Doctrine DBAL (#426)
67
- Added missing `capture-soft-fails` config schema option (#417)
78
- Deprecate the `Sentry\SentryBundle\EventListener\ConsoleCommandListener` class in favor of its parent class `Sentry\SentryBundle\EventListener\ConsoleListener` (#429)

composer.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,14 @@
5050
"symfony/monolog-bundle": "^3.4",
5151
"symfony/phpunit-bridge": "^5.0",
5252
"symfony/polyfill-php80": "^1.22",
53+
"symfony/twig-bundle": "^3.4.44||^4.4.12||^5.0.11",
5354
"symfony/yaml": "^3.4.44||^4.4.11||^5.0.11",
5455
"vimeo/psalm": "^4.3"
5556
},
5657
"suggest": {
5758
"monolog/monolog": "Allow sending log messages to Sentry by using the included Monolog handler.",
58-
"doctrine/doctrine-bundle": "Allow distributed tracing of database queries using Sentry."
59+
"doctrine/doctrine-bundle": "Allow distributed tracing of database queries using Sentry.",
60+
"symfony/twig-bundle": "Allow distributed tracing of twig template rendering using Sentry."
5961
},
6062
"autoload": {
6163
"files": [

phpstan-baseline.neon

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,16 @@ parameters:
9595
count: 1
9696
path: src/Tracing/Doctrine/DBAL/TracingDriverConnection.php
9797

98+
-
99+
message: "#^Method Sentry\\\\SentryBundle\\\\Tracing\\\\Twig\\\\TwigTracingExtension\\:\\:enter\\(\\) has parameter \\$profile with no value type specified in iterable type Twig\\\\Profiler\\\\Profile\\.$#"
100+
count: 1
101+
path: src/Tracing/Twig/TwigTracingExtension.php
102+
103+
-
104+
message: "#^Method Sentry\\\\SentryBundle\\\\Tracing\\\\Twig\\\\TwigTracingExtension\\:\\:leave\\(\\) has parameter \\$profile with no value type specified in iterable type Twig\\\\Profiler\\\\Profile\\.$#"
105+
count: 1
106+
path: src/Tracing/Twig/TwigTracingExtension.php
107+
98108
-
99109
message: "#^Class Symfony\\\\Component\\\\HttpKernel\\\\Event\\\\GetResponseForExceptionEvent not found\\.$#"
100110
count: 1
@@ -219,4 +229,3 @@ parameters:
219229
message: "#^Trying to mock an undefined method convertException\\(\\) on class Sentry\\\\SentryBundle\\\\Tests\\\\Tracing\\\\Doctrine\\\\DBAL\\\\StubExceptionConverterDriverInterface\\.$#"
220230
count: 1
221231
path: tests/Tracing/Doctrine/DBAL/TracingDriverTest.php
222-

src/DependencyInjection/Configuration.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,9 @@ private function addDistributedTracingSection(ArrayNodeDefinition $rootNode): vo
163163
->end()
164164
->end()
165165
->end()
166+
->arrayNode('twig')
167+
->canBeEnabled()
168+
->end()
166169
->end()
167170
->end()
168171
->end();

src/DependencyInjection/SentryExtension.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use Sentry\SentryBundle\SentryBundle;
2020
use Sentry\SentryBundle\Tracing\Doctrine\DBAL\ConnectionConfigurator;
2121
use Sentry\SentryBundle\Tracing\Doctrine\DBAL\TracingDriverMiddleware;
22+
use Sentry\SentryBundle\Tracing\Twig\TwigTracingExtension;
2223
use Sentry\Serializer\RepresentationSerializer;
2324
use Sentry\Serializer\Serializer;
2425
use Sentry\Transport\TransportFactoryInterface;
@@ -62,6 +63,7 @@ protected function loadInternal(array $mergedConfig, ContainerBuilder $container
6263
$this->registerErrorListenerConfiguration($container, $mergedConfig);
6364
$this->registerMessengerListenerConfiguration($container, $mergedConfig['messenger']);
6465
$this->registerTracingConfiguration($container, $mergedConfig['tracing']);
66+
$this->registerTracingTwigExtensionConfiguration($container, $mergedConfig['tracing']);
6567
}
6668

6769
/**
@@ -173,6 +175,18 @@ private function registerTracingConfiguration(ContainerBuilder $container, array
173175
}
174176
}
175177

178+
/**
179+
* @param array<string, mixed> $config
180+
*/
181+
private function registerTracingTwigExtensionConfiguration(ContainerBuilder $container, array $config): void
182+
{
183+
$isConfigEnabled = $this->isConfigEnabled($container, $config['twig']);
184+
185+
if (!$isConfigEnabled) {
186+
$container->removeDefinition(TwigTracingExtension::class);
187+
}
188+
}
189+
176190
/**
177191
* @param string[] $integrations
178192
* @param array<string, mixed> $config

src/Resources/config/schema/sentry-1.0.xsd

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
<xsd:complexType name="tracing">
8585
<xsd:choice maxOccurs="unbounded">
8686
<xsd:element name="dbal" type="tracing-dbal" minOccurs="0" maxOccurs="1" />
87+
<xsd:element name="twig" type="tracing-twig" minOccurs="0" maxOccurs="1" />
8788
</xsd:choice>
8889
</xsd:complexType>
8990

@@ -94,4 +95,7 @@
9495

9596
<xsd:attribute name="enabled" type="xsd:boolean" />
9697
</xsd:complexType>
98+
<xsd:complexType name="tracing-twig">
99+
<xsd:attribute name="enabled" type="xsd:boolean" />
100+
</xsd:complexType>
97101
</xsd:schema>

src/Resources/config/services.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@
7373
<argument type="service" id="Sentry\SentryBundle\Tracing\Doctrine\DBAL\TracingDriverMiddleware" />
7474
</service>
7575

76+
<service id="Sentry\SentryBundle\Tracing\Twig\TwigTracingExtension" class="Sentry\SentryBundle\Tracing\Twig\TwigTracingExtension">
77+
<argument type="service" id="Sentry\State\HubInterface" />
78+
<tag name="twig.extension" />
79+
</service>
80+
7681
<service id="Sentry\Integration\RequestFetcherInterface" class="Sentry\SentryBundle\Integration\RequestFetcher">
7782
<argument type="service" id="Symfony\Component\HttpFoundation\RequestStack" />
7883
<argument type="service" id="Symfony\Bridge\PsrHttpMessage\HttpMessageFactoryInterface" on-invalid="null" />
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sentry\SentryBundle\Tracing\Twig;
6+
7+
use Sentry\State\HubInterface;
8+
use Sentry\Tracing\SpanContext;
9+
use SplObjectStorage;
10+
use Twig\Extension\AbstractExtension;
11+
use Twig\Profiler\NodeVisitor\ProfilerNodeVisitor;
12+
use Twig\Profiler\Profile;
13+
14+
final class TwigTracingExtension extends AbstractExtension
15+
{
16+
/**
17+
* @var HubInterface The current hub
18+
*/
19+
private $hub;
20+
21+
/**
22+
* @var SplObjectStorage<object, \Sentry\Tracing\Span>
23+
*/
24+
private $events;
25+
26+
/**
27+
* @param HubInterface $hub The current hub
28+
*/
29+
public function __construct(HubInterface $hub)
30+
{
31+
$this->hub = $hub;
32+
$this->events = new SplObjectStorage();
33+
}
34+
35+
public function enter(Profile $profile): void
36+
{
37+
$transaction = $this->hub->getTransaction();
38+
39+
if (null === $transaction || !$profile->isTemplate()) {
40+
return;
41+
}
42+
43+
$spanContext = new SpanContext();
44+
$spanContext->setOp('twig.template');
45+
$spanContext->setDescription($profile->getName());
46+
47+
$this->events[$profile] = $transaction->startChild($spanContext);
48+
}
49+
50+
public function leave(Profile $profile): void
51+
{
52+
if (empty($this->events[$profile]) || !$profile->isTemplate()) {
53+
return;
54+
}
55+
56+
$this->events[$profile]->finish();
57+
unset($this->events[$profile]);
58+
}
59+
60+
public function getNodeVisitors(): array
61+
{
62+
return [new ProfilerNodeVisitor(static::class)];
63+
}
64+
}

tests/DependencyInjection/ConfigurationTest.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ public function testProcessConfigurationWithDefaultConfiguration(): void
4141
'enabled' => false,
4242
'connections' => class_exists(DoctrineBundle::class) ? ['%doctrine.default_connection%'] : [],
4343
],
44+
'twig' => [
45+
'enabled' => false,
46+
],
4447
],
4548
];
4649

tests/DependencyInjection/Fixtures/php/full.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,8 @@
4747
'enabled' => false,
4848
'connections' => ['default'],
4949
],
50+
'twig' => [
51+
'enabled' => false,
52+
],
5053
],
5154
]);
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Symfony\Component\DependencyInjection\ContainerBuilder;
6+
7+
/** @var ContainerBuilder $container */
8+
$container->loadFromExtension('sentry', [
9+
'tracing' => [
10+
'twig' => [
11+
'enabled' => true,
12+
],
13+
],
14+
]);

tests/DependencyInjection/Fixtures/xml/full.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
<sentry:dbal enabled="false">
4242
<sentry:connection>default</sentry:connection>
4343
</sentry:dbal>
44+
<sentry:twig enabled="false" />
4445
</sentry:tracing>
4546
</sentry:config>
4647
</container>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?xml version="1.0" ?>
2+
3+
<container xmlns="http://symfony.com/schema/dic/services"
4+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5+
xmlns:sentry="https://sentry.io/schema/dic/sentry-symfony"
6+
xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd
7+
https://sentry.io/schema/dic/sentry-symfony https://sentry.io/schema/dic/sentry-symfony/sentry-1.0.xsd">
8+
9+
<sentry:config>
10+
<sentry:tracing>
11+
<sentry:twig enabled="true" />
12+
</sentry:tracing>
13+
</sentry:config>
14+
</container>

tests/DependencyInjection/Fixtures/yml/full.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,5 @@ sentry:
4242
enabled: false
4343
connections:
4444
- enabled
45+
twig:
46+
enabled: false
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
sentry:
2+
tracing:
3+
twig:
4+
enabled: true

tests/DependencyInjection/SentryExtensionTest.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use Sentry\SentryBundle\SentryBundle;
2020
use Sentry\SentryBundle\Tracing\Doctrine\DBAL\ConnectionConfigurator;
2121
use Sentry\SentryBundle\Tracing\Doctrine\DBAL\TracingDriverMiddleware;
22+
use Sentry\SentryBundle\Tracing\Twig\TwigTracingExtension;
2223
use Sentry\Serializer\RepresentationSerializer;
2324
use Sentry\Serializer\Serializer;
2425
use Sentry\Transport\TransportFactoryInterface;
@@ -286,6 +287,20 @@ public function testTracingDriverMiddlewareIsRemovedWhenDbalTracingIsDisabled():
286287
$this->assertEmpty($container->getParameter('sentry.tracing.dbal.connections'));
287288
}
288289

290+
public function testTwigTracingExtensionIsConfiguredWhenTwigTracingIsEnabled(): void
291+
{
292+
$container = $this->createContainerFromFixture('twig_tracing_enabled');
293+
294+
$this->assertTrue($container->hasDefinition(TwigTracingExtension::class));
295+
}
296+
297+
public function testTwigTracingExtensionIsRemovedWhenTwigTracingIsDisabled(): void
298+
{
299+
$container = $this->createContainerFromFixture('full');
300+
301+
$this->assertFalse($container->hasDefinition(TwigTracingExtension::class));
302+
}
303+
289304
private function createContainerFromFixture(string $fixtureFile): ContainerBuilder
290305
{
291306
$container = new ContainerBuilder(new EnvPlaceholderParameterBag([
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sentry\SentryBundle\Tests\Tracing\Twig;
6+
7+
use PHPUnit\Framework\MockObject\MockObject;
8+
use PHPUnit\Framework\TestCase;
9+
use Sentry\SentryBundle\Tracing\Twig\TwigTracingExtension;
10+
use Sentry\State\HubInterface;
11+
use Sentry\Tracing\Transaction;
12+
use Sentry\Tracing\TransactionContext;
13+
use Twig\Profiler\Profile;
14+
15+
final class TwigTracingExtensionTest extends TestCase
16+
{
17+
/**
18+
* @var MockObject&HubInterface
19+
*/
20+
private $hub;
21+
22+
/**
23+
* @var TwigTracingExtension
24+
*/
25+
private $listener;
26+
27+
protected function setUp(): void
28+
{
29+
$this->hub = $this->createMock(HubInterface::class);
30+
$this->listener = new TwigTracingExtension($this->hub);
31+
}
32+
33+
public function testThatTwigEnterProfileIgnoresTracingWhenTransactionIsNotStarted(): void
34+
{
35+
$this->hub->expects($this->once())
36+
->method('getTransaction')
37+
->willReturn(null);
38+
39+
$this->listener->enter(new Profile('main', Profile::TEMPLATE));
40+
}
41+
42+
public function testThatTwigEnterProfileIgnoresTracingWhenNotATemplate(): void
43+
{
44+
$this->hub->expects($this->once())
45+
->method('getTransaction')
46+
->willReturn(new Transaction(new TransactionContext()));
47+
48+
$this->listener->enter(new Profile('main', Profile::ROOT));
49+
}
50+
51+
public function testThatTwigLeaveProfileIgnoresTracingWhenTransactionIsNotStarted(): void
52+
{
53+
$this->hub->expects($this->once())
54+
->method('getTransaction')
55+
->willReturn(null);
56+
57+
$profile = new Profile('main', Profile::TEMPLATE);
58+
59+
$this->listener->enter($profile);
60+
$this->listener->leave($profile);
61+
}
62+
63+
public function testThatTwigLeaveProfileIgnoresTracingWhenNotATemplate(): void
64+
{
65+
$this->hub->expects($this->once())
66+
->method('getTransaction')
67+
->willReturn(new Transaction(new TransactionContext()));
68+
69+
$profile = new Profile('main', Profile::ROOT);
70+
71+
$this->listener->enter($profile);
72+
$this->listener->leave($profile);
73+
}
74+
75+
public function testThatTwigEnterProfileAttachesAChildSpanWhenTransactionStarted(): void
76+
{
77+
$transaction = new Transaction(new TransactionContext());
78+
$transaction->initSpanRecorder();
79+
80+
$this->hub->expects($this->once())
81+
->method('getTransaction')
82+
->willReturn($transaction);
83+
84+
$this->listener->enter(new Profile('main', Profile::TEMPLATE));
85+
86+
$spans = $transaction->getSpanRecorder()->getSpans();
87+
88+
$this->assertCount(2, $spans);
89+
$this->assertNull($spans[1]->getEndTimestamp());
90+
}
91+
92+
public function testThatTwigLeaveProfileFinishesTheChildSpanWhenChildSpanStarted(): void
93+
{
94+
$transaction = new Transaction(new TransactionContext());
95+
$transaction->initSpanRecorder();
96+
97+
$this->hub->expects($this->once())
98+
->method('getTransaction')
99+
->willReturn($transaction);
100+
101+
$profile = new Profile('main', Profile::TEMPLATE);
102+
103+
$this->listener->enter($profile);
104+
$this->listener->leave($profile);
105+
106+
$spans = $transaction->getSpanRecorder()->getSpans();
107+
108+
$this->assertCount(2, $spans);
109+
$this->assertNotNull($spans[1]->getEndTimestamp());
110+
}
111+
}

0 commit comments

Comments
 (0)