Skip to content

Commit

Permalink
Merge pull request #163 from City-of-Helsinki/UHF-9635
Browse files Browse the repository at this point in the history
UHF-9635: Custom Sentry traces_sampler callback
  • Loading branch information
tuutti authored Apr 19, 2024
2 parents a4a0aaf + e94a83d commit d063d49
Show file tree
Hide file tree
Showing 6 changed files with 239 additions and 1 deletion.
10 changes: 10 additions & 0 deletions helfi_api_base.install
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,16 @@ function helfi_api_base_install(bool $is_syncing = FALSE) : void {
}
catch (\InvalidArgumentException) {
}

if (Drupal::moduleHandler()->moduleExists('raven')) {
Drupal::configFactory()->getEditable('raven.settings')
->set('drush_error_handler', TRUE)
->set('request_tracing', TRUE)
->set('traces_sample_rate', 0.2)
->set('database_tracing', TRUE)
->set('twig_tracing', TRUE)
->save();
}
}

/**
Expand Down
1 change: 1 addition & 0 deletions helfi_api_base.services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ services:
parent: logger.channel_base
arguments: [ 'helfi_api_base' ]

Drupal\helfi_api_base\EventSubscriber\SentryTracesSamplerSubscriber: ~
Drupal\helfi_api_base\EventSubscriber\DisableUserPasswordSubscriber: ~
Drupal\helfi_api_base\Features\FeatureManager: ~

Expand Down
71 changes: 71 additions & 0 deletions src/EventSubscriber/SentryTracesSamplerSubscriber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

declare(strict_types=1);

namespace Drupal\helfi_api_base\EventSubscriber;

use Drupal\raven\Event\OptionsAlter;
use Sentry\Tracing\SamplingContext;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
* Overrides the Sentry traces sampler to ignore certain data.
*/
final class SentryTracesSamplerSubscriber implements EventSubscriberInterface {

public const DEFAULT_SAMPLE_RATE = 0.2;

/**
* Checks if the trace url should be ignored.
*
* @param array $data
* The data.
*
* @return bool
* TRUE if url should be ignored.
*/
private function ignoreTracerUrl(array $data) : bool {
if (!isset($data['http.url'])) {
return FALSE;
}
$path = parse_url($data['http.url'], PHP_URL_PATH);

return str_ends_with($path, '/health');
}

/**
* Responds to OptionsAlter event.
*
* @param \Drupal\raven\Event\OptionsAlter $event
* The options alter event.
*/
public function setTracesSampler(OptionsAlter $event) : void {
$event->options['traces_sampler'] = function (SamplingContext $context) use ($event): float {
if ($context->getParentSampled()) {
// If the parent transaction (for example, a JavaScript front-end)
// is sampled, also sample the current transaction.
return 1.0;
}

$data = $context->getTransactionContext()?->getData();

if ($data && $this->ignoreTracerUrl($data)) {
return 0;
}

// Sample ~20% of transactions by default.
// @see https://docs.sentry.io/platforms/php/configuration/sampling/
return $event->options['traces_sample_rate'] ?? self::DEFAULT_SAMPLE_RATE;
};
}

/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() : array {
return [
'Drupal\raven\Event\OptionsAlter' => ['setTracesSampler'],
];
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

declare(strict_types=1);

namespace Drupal\Tests\helfi_api_base\Kernel\EventSubscriber;

use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\helfi_api_base\Traits\ApiTestTrait;
use Sentry\SentrySdk;

/**
* Tests our custom Sentry 'traces_sampler' event subscriber.
*
* @group helfi_api_base
*/
class SentryTracesSamplerSubscriberTest extends KernelTestBase {

use ApiTestTrait;

/**
* {@inheritdoc}
*/
protected static $modules = [
'user',
'raven',
'helfi_api_base',
];

/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();

$this->installConfig('helfi_api_base');
}

/**
* Make sure our 'traces_sampler' is actually registered.
*/
public function testSamplerIsCalled() : void {
$request = $this->getMockedRequest('/user/login');
$this->processRequest($request);

$client = SentrySdk::getCurrentHub()->getClient();
$sampler = $client->getOptions()->getTracesSampler();
$this->assertIsCallable($sampler);
}

}
7 changes: 6 additions & 1 deletion tests/src/Traits/ApiTestTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,12 @@ public function fromOptions(array $config = []) : Client {
* The response.
*/
protected function processRequest(Request $request): Response {
return $this->container->get('http_kernel')->handle($request);
/** @var \Drupal\Core\StackMiddleware\StackedHttpKernel $kernel */
$kernel = $this->container->get('http_kernel');
$response = $kernel->handle($request);
$kernel->terminate($request, $response);

return $response;
}

/**
Expand Down
101 changes: 101 additions & 0 deletions tests/src/Unit/EventSubscriber/SentryTracesSamplerSubscriberTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?php

declare(strict_types=1);

namespace Drupal\Tests\helfi_api_base\Unit\EventSubscriber;

use Drupal\helfi_api_base\EventSubscriber\SentryTracesSamplerSubscriber;
use Drupal\raven\Event\OptionsAlter;
use Drupal\Tests\UnitTestCase;
use Sentry\Tracing\SamplingContext;
use Sentry\Tracing\TransactionContext;

/**
* Tests sentry traces sample.
*
* @group helfi_api_base
*/
class SentryTracesSamplerSubscriberTest extends UnitTestCase {

/**
* Calls the sampler callback.
*
* @param array $options
* The default options.
* @param \Sentry\Tracing\SamplingContext $context
* The sampling context.
*
* @return float
* The sample rate.
*/
private function callSampler(array $options, SamplingContext $context) : float {
$sut = new SentryTracesSamplerSubscriber();
$sut->setTracesSampler(new OptionsAlter($options));

$this->assertIsCallable($options['traces_sampler']);

return $options['traces_sampler']($context);
}

/**
* Tests the default sample rate value.
*/
public function testDefaultSamplerValue() : void {
$rate = $this->callSampler([], new SamplingContext());
$this->assertEquals(SentryTracesSamplerSubscriber::DEFAULT_SAMPLE_RATE, $rate);
}

/**
* Make sure the sample rate is inherited from parent.
*/
public function testParentSampledValue() : void {
$rate = $this->callSampler([], (new SamplingContext())->setParentSampled(TRUE));
$this->assertEquals(1.0, $rate);
}

/**
* Make sure the sample rate is inherited from the setting.
*/
public function testParentDefaultValue() : void {
$rate = $this->callSampler(['traces_sample_rate' => 0.5], (new SamplingContext()));
$this->assertEquals(0.5, $rate);
}

/**
* Make sure nothing is done when URL context is not set.
*/
public function testTracerNoUrl() : void {
$transaction = new TransactionContext();
$transaction->setData(['something' => 'else']);
$context = SamplingContext::getDefault($transaction);
$rate = $this->callSampler([], $context);
$this->assertEquals(SentryTracesSamplerSubscriber::DEFAULT_SAMPLE_RATE, $rate);
}

/**
* Make sure the sample rate is set zero when we get a URL match.
*
* @dataProvider ignoreUrlData
*/
public function testTracerIgnoreUrl(string $url) : void {
$transaction = new TransactionContext();
$transaction->setData(['http.url' => $url]);
$context = SamplingContext::getDefault($transaction);
$rate = $this->callSampler([], $context);
$this->assertEquals(0, $rate);
}

/**
* Data provider.
*
* @return array[]
* The data.
*/
public function ignoreUrlData() : array {
return [
['http://localhost/fi/health'],
['https://localhost/health'],
];
}

}

0 comments on commit d063d49

Please sign in to comment.