From a9ada243719987a2e191c84fd892671db01d42d9 Mon Sep 17 00:00:00 2001 From: Michi Hoffmann Date: Mon, 11 Aug 2025 13:27:55 +0200 Subject: [PATCH 01/16] Refactor trace header parsing logic (#1876) --- phpstan-baseline.neon | 5 + src/Tracing/PropagationContext.php | 58 +++------- src/Tracing/Traits/TraceHeaderParserTrait.php | 106 ++++++++++++++++++ src/Tracing/TransactionContext.php | 65 +++-------- 4 files changed, 143 insertions(+), 91 deletions(-) create mode 100644 src/Tracing/Traits/TraceHeaderParserTrait.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 5d7f1c7334..3ff509ab0e 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -310,6 +310,11 @@ parameters: count: 1 path: src/Tracing/GuzzleTracingMiddleware.php + - + message: "#^Property Sentry\\\\Tracing\\\\PropagationContext\\:\\:\\$parentSampled is never read, only written\\.$#" + count: 1 + path: src/Tracing/PropagationContext.php + - message: "#^Method Sentry\\\\Tracing\\\\Span\\:\\:getMetricsSummary\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 diff --git a/src/Tracing/PropagationContext.php b/src/Tracing/PropagationContext.php index 363dc71d1c..a4e8551764 100644 --- a/src/Tracing/PropagationContext.php +++ b/src/Tracing/PropagationContext.php @@ -6,10 +6,11 @@ use Sentry\SentrySdk; use Sentry\State\Scope; +use Sentry\Tracing\Traits\TraceHeaderParserTrait; final class PropagationContext { - private const SENTRY_TRACEPARENT_HEADER_REGEX = '/^[ \\t]*(?[0-9a-f]{32})?-?(?[0-9a-f]{16})?-?(?[01])?[ \\t]*$/i'; + use TraceHeaderParserTrait; /** * @var TraceId The trace id @@ -183,60 +184,29 @@ public function setSampleRand(?float $sampleRand): self return $this; } - // TODO add same logic as in TransactionContext private static function parseTraceparentAndBaggage(string $traceparent, string $baggage): self { $context = self::fromDefaults(); - $hasSentryTrace = false; + $parsedData = self::parseTraceAndBaggageHeaders($traceparent, $baggage); - if (preg_match(self::SENTRY_TRACEPARENT_HEADER_REGEX, $traceparent, $matches)) { - if (!empty($matches['trace_id'])) { - $context->traceId = new TraceId($matches['trace_id']); - $hasSentryTrace = true; - } - - if (!empty($matches['span_id'])) { - $context->parentSpanId = new SpanId($matches['span_id']); - $hasSentryTrace = true; - } - - if (isset($matches['sampled'])) { - $context->parentSampled = $matches['sampled'] === '1'; - $hasSentryTrace = true; - } + if ($parsedData['traceId'] !== null) { + $context->traceId = $parsedData['traceId']; } - $samplingContext = DynamicSamplingContext::fromHeader($baggage); + if ($parsedData['parentSpanId'] !== null) { + $context->parentSpanId = $parsedData['parentSpanId']; + } - if ($hasSentryTrace && !$samplingContext->hasEntries()) { - // The request comes from an old SDK which does not support Dynamic Sampling. - // Propagate the Dynamic Sampling Context as is, but frozen, even without sentry-* entries. - $samplingContext->freeze(); - $context->dynamicSamplingContext = $samplingContext; + if ($parsedData['parentSampled'] !== null) { + $context->parentSampled = $parsedData['parentSampled']; } - if ($hasSentryTrace && $samplingContext->hasEntries()) { - // The baggage header contains Dynamic Sampling Context data from an upstream SDK. - // Propagate this Dynamic Sampling Context. - $context->dynamicSamplingContext = $samplingContext; + if ($parsedData['dynamicSamplingContext'] !== null) { + $context->dynamicSamplingContext = $parsedData['dynamicSamplingContext']; } - // Store the propagated trace sample rand or generate a new one - if ($samplingContext->has('sample_rand')) { - $context->sampleRand = (float) $samplingContext->get('sample_rand'); - } else { - if ($samplingContext->has('sample_rate') && $context->parentSampled !== null) { - if ($context->parentSampled === true) { - // [0, rate) - $context->sampleRand = round(mt_rand(0, mt_getrandmax() - 1) / mt_getrandmax() * (float) $samplingContext->get('sample_rate'), 6); - } else { - // [rate, 1) - $context->sampleRand = round(mt_rand(0, mt_getrandmax() - 1) / mt_getrandmax() * (1 - (float) $samplingContext->get('sample_rate')) + (float) $samplingContext->get('sample_rate'), 6); - } - } elseif ($context->parentSampled !== null) { - // [0, 1) - $context->sampleRand = round(mt_rand(0, mt_getrandmax() - 1) / mt_getrandmax(), 6); - } + if ($parsedData['sampleRand'] !== null) { + $context->sampleRand = $parsedData['sampleRand']; } return $context; diff --git a/src/Tracing/Traits/TraceHeaderParserTrait.php b/src/Tracing/Traits/TraceHeaderParserTrait.php new file mode 100644 index 0000000000..56e002d68b --- /dev/null +++ b/src/Tracing/Traits/TraceHeaderParserTrait.php @@ -0,0 +1,106 @@ +[0-9a-f]{32})?-?(?[0-9a-f]{16})?-?(?[01])?[ \\t]*$/i'; + + /** + * Parses the sentry-trace and baggage headers and returns the extracted data. + * + * @param string $sentryTrace The sentry-trace header value + * @param string $baggage The baggage header value + * + * @return array{ + * traceId: TraceId|null, + * parentSpanId: SpanId|null, + * parentSampled: bool|null, + * dynamicSamplingContext: DynamicSamplingContext|null, + * sampleRand: float|null, + * parentSamplingRate: float|null + * } + */ + protected static function parseTraceAndBaggageHeaders(string $sentryTrace, string $baggage): array + { + $result = [ + 'traceId' => null, + 'parentSpanId' => null, + 'parentSampled' => null, + 'dynamicSamplingContext' => null, + 'sampleRand' => null, + 'parentSamplingRate' => null, + ]; + + $hasSentryTrace = false; + + if (preg_match(self::$sentryTraceparentHeaderRegex, $sentryTrace, $matches)) { + if (!empty($matches['trace_id'])) { + $result['traceId'] = new TraceId($matches['trace_id']); + $hasSentryTrace = true; + } + + if (!empty($matches['span_id'])) { + $result['parentSpanId'] = new SpanId($matches['span_id']); + $hasSentryTrace = true; + } + + if (isset($matches['sampled'])) { + $result['parentSampled'] = $matches['sampled'] === '1'; + $hasSentryTrace = true; + } + } + + $samplingContext = DynamicSamplingContext::fromHeader($baggage); + + if ($hasSentryTrace && !$samplingContext->hasEntries()) { + // The request comes from an old SDK which does not support Dynamic Sampling. + // Propagate the Dynamic Sampling Context as is, but frozen, even without sentry-* entries. + $samplingContext->freeze(); + $result['dynamicSamplingContext'] = $samplingContext; + } + + if ($hasSentryTrace && $samplingContext->hasEntries()) { + // The baggage header contains Dynamic Sampling Context data from an upstream SDK. + // Propagate this Dynamic Sampling Context. + $result['dynamicSamplingContext'] = $samplingContext; + } + + // Store the propagated traces sample rate + if ($samplingContext->has('sample_rate')) { + $result['parentSamplingRate'] = (float) $samplingContext->get('sample_rate'); + } + + // Store the propagated trace sample rand or generate a new one + if ($samplingContext->has('sample_rand')) { + $result['sampleRand'] = (float) $samplingContext->get('sample_rand'); + } else { + if ($samplingContext->has('sample_rate') && $result['parentSampled'] !== null) { + if ($result['parentSampled'] === true) { + // [0, rate) + $result['sampleRand'] = round(mt_rand(0, mt_getrandmax() - 1) / mt_getrandmax() * (float) $samplingContext->get('sample_rate'), 6); + } else { + // [rate, 1) + $result['sampleRand'] = round(mt_rand(0, mt_getrandmax() - 1) / mt_getrandmax() * (1 - (float) $samplingContext->get('sample_rate')) + (float) $samplingContext->get('sample_rate'), 6); + } + } elseif ($result['parentSampled'] !== null) { + // [0, 1) + $result['sampleRand'] = round(mt_rand(0, mt_getrandmax() - 1) / mt_getrandmax(), 6); + } + } + + return $result; + } +} diff --git a/src/Tracing/TransactionContext.php b/src/Tracing/TransactionContext.php index 95ba789796..a4c55c04af 100644 --- a/src/Tracing/TransactionContext.php +++ b/src/Tracing/TransactionContext.php @@ -4,9 +4,11 @@ namespace Sentry\Tracing; +use Sentry\Tracing\Traits\TraceHeaderParserTrait; + final class TransactionContext extends SpanContext { - private const SENTRY_TRACEPARENT_HEADER_REGEX = '/^[ \\t]*(?[0-9a-f]{32})?-?(?[0-9a-f]{16})?-?(?[01])?[ \\t]*$/i'; + use TraceHeaderParserTrait; public const DEFAULT_NAME = ''; @@ -147,61 +149,30 @@ public static function fromHeaders(string $sentryTraceHeader, string $baggageHea private static function parseTraceAndBaggage(string $sentryTrace, string $baggage): self { $context = new self(); - $hasSentryTrace = false; - - if (preg_match(self::SENTRY_TRACEPARENT_HEADER_REGEX, $sentryTrace, $matches)) { - if (!empty($matches['trace_id'])) { - $context->traceId = new TraceId($matches['trace_id']); - $hasSentryTrace = true; - } - - if (!empty($matches['span_id'])) { - $context->parentSpanId = new SpanId($matches['span_id']); - $hasSentryTrace = true; - } - - if (isset($matches['sampled'])) { - $context->parentSampled = $matches['sampled'] === '1'; - $hasSentryTrace = true; - } + $parsedData = self::parseTraceAndBaggageHeaders($sentryTrace, $baggage); + + if ($parsedData['traceId'] !== null) { + $context->traceId = $parsedData['traceId']; } - $samplingContext = DynamicSamplingContext::fromHeader($baggage); + if ($parsedData['parentSpanId'] !== null) { + $context->parentSpanId = $parsedData['parentSpanId']; + } - if ($hasSentryTrace && !$samplingContext->hasEntries()) { - // The request comes from an old SDK which does not support Dynamic Sampling. - // Propagate the Dynamic Sampling Context as is, but frozen, even without sentry-* entries. - $samplingContext->freeze(); - $context->getMetadata()->setDynamicSamplingContext($samplingContext); + if ($parsedData['parentSampled'] !== null) { + $context->parentSampled = $parsedData['parentSampled']; } - if ($hasSentryTrace && $samplingContext->hasEntries()) { - // The baggage header contains Dynamic Sampling Context data from an upstream SDK. - // Propagate this Dynamic Sampling Context. - $context->getMetadata()->setDynamicSamplingContext($samplingContext); + if ($parsedData['dynamicSamplingContext'] !== null) { + $context->getMetadata()->setDynamicSamplingContext($parsedData['dynamicSamplingContext']); } - // Store the propagated traces sample rate - if ($samplingContext->has('sample_rate')) { - $context->getMetadata()->setParentSamplingRate((float) $samplingContext->get('sample_rate')); + if ($parsedData['parentSamplingRate'] !== null) { + $context->getMetadata()->setParentSamplingRate($parsedData['parentSamplingRate']); } - // Store the propagated trace sample rand or generate a new one - if ($samplingContext->has('sample_rand')) { - $context->getMetadata()->setSampleRand((float) $samplingContext->get('sample_rand')); - } else { - if ($samplingContext->has('sample_rate') && $context->parentSampled !== null) { - if ($context->parentSampled === true) { - // [0, rate) - $context->getMetadata()->setSampleRand(round(mt_rand(0, mt_getrandmax() - 1) / mt_getrandmax() * (float) $samplingContext->get('sample_rate'), 6)); - } else { - // [rate, 1) - $context->getMetadata()->setSampleRand(round(mt_rand(0, mt_getrandmax() - 1) / mt_getrandmax() * (1 - (float) $samplingContext->get('sample_rate')) + (float) $samplingContext->get('sample_rate'), 6)); - } - } elseif ($context->parentSampled !== null) { - // [0, 1) - $context->getMetadata()->setSampleRand(round(mt_rand(0, mt_getrandmax() - 1) / mt_getrandmax(), 6)); - } + if ($parsedData['sampleRand'] !== null) { + $context->getMetadata()->setSampleRand($parsedData['sampleRand']); } return $context; From 0bc8a0592a57c1ab47ce70909860fb9eceeeee35 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Aug 2025 11:04:51 +0200 Subject: [PATCH 02/16] chore(deps): bump actions/checkout from 4 to 5 (#1878) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- .github/workflows/publish-release.yaml | 2 +- .github/workflows/static-analysis.yaml | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3f95de17b5..8bc0b32936 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,7 +41,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 2 diff --git a/.github/workflows/publish-release.yaml b/.github/workflows/publish-release.yaml index 5488ac5b01..9dfdd74844 100644 --- a/.github/workflows/publish-release.yaml +++ b/.github/workflows/publish-release.yaml @@ -29,7 +29,7 @@ jobs: app-id: ${{ vars.SENTRY_RELEASE_BOT_CLIENT_ID }} private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }} - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: token: ${{ steps.token.outputs.token }} fetch-depth: 0 diff --git a/.github/workflows/static-analysis.yaml b/.github/workflows/static-analysis.yaml index 69de38c90d..8be436451c 100644 --- a/.github/workflows/static-analysis.yaml +++ b/.github/workflows/static-analysis.yaml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -34,7 +34,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -52,7 +52,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 2 # needed by codecov sometimes From 458c0cfe93c4e316fa8702046998310db0c548e5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Aug 2025 11:04:58 +0200 Subject: [PATCH 03/16] chore(deps): bump actions/create-github-app-token from 2.0.6 to 2.1.1 (#1879) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/publish-release.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish-release.yaml b/.github/workflows/publish-release.yaml index 9dfdd74844..207c12f944 100644 --- a/.github/workflows/publish-release.yaml +++ b/.github/workflows/publish-release.yaml @@ -24,7 +24,7 @@ jobs: steps: - name: Get auth token id: token - uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6 + uses: actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b # v2.1.1 with: app-id: ${{ vars.SENTRY_RELEASE_BOT_CLIENT_ID }} private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }} From e2e6cea3b52690bd91ebaa431f4b806b5a749d0d Mon Sep 17 00:00:00 2001 From: Alex Bouma Date: Wed, 20 Aug 2025 08:18:18 -0600 Subject: [PATCH 04/16] Fix non string indexed attributes passed as log attributes (#1882) --- .php-cs-fixer.dist.php | 1 + src/Logs/LogsAggregator.php | 16 +++++++- tests/Logs/LogsAggregatorTest.php | 62 +++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 2 deletions(-) diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 6a94c4c5c7..9cf23f9120 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -34,6 +34,7 @@ 'after_heredoc' => false, 'elements' => ['arrays'], ], + 'no_whitespace_before_comma_in_array' => false, // Should be dropped when we drop support for PHP 7.x ]) ->setRiskyAllowed(true) ->setFinder( diff --git a/src/Logs/LogsAggregator.php b/src/Logs/LogsAggregator.php index e4587b124c..ba1cc68b33 100644 --- a/src/Logs/LogsAggregator.php +++ b/src/Logs/LogsAggregator.php @@ -108,15 +108,27 @@ public function add( foreach ($attributes as $key => $value) { $attribute = Attribute::tryFromValue($value); + if (!\is_string($key)) { + if ($sdkLogger !== null) { + $sdkLogger->info( + \sprintf("Dropping log attribute with non-string key '%s' and value of type '%s'.", $key, \gettype($value)) + ); + } + + continue; + } + if ($attribute === null) { if ($sdkLogger !== null) { $sdkLogger->info( \sprintf("Dropping log attribute {$key} with value of type '%s' because it is not serializable or an unsupported type.", \gettype($value)) ); } - } else { - $log->setAttribute($key, $attribute); + + continue; } + + $log->setAttribute($key, $attribute); } $log = ($options->getBeforeSendLogCallback())($log); diff --git a/tests/Logs/LogsAggregatorTest.php b/tests/Logs/LogsAggregatorTest.php index 7e5e38b988..d60aaf5536 100644 --- a/tests/Logs/LogsAggregatorTest.php +++ b/tests/Logs/LogsAggregatorTest.php @@ -20,6 +20,68 @@ final class LogsAggregatorTest extends TestCase { + /** + * This test is kept simple to ensure the `LogAggregator` is able to handle attributes passed in different formats. + * + * Extensive testing of attributes is done in the `Attributes/*` test classes. + * + * @dataProvider attributesDataProvider + */ + public function testAttributes(array $attributes, array $expected): void + { + $client = ClientBuilder::create([ + 'enable_logs' => true, + ])->getClient(); + + $hub = new Hub($client); + SentrySdk::setCurrentHub($hub); + + $aggregator = new LogsAggregator(); + + $aggregator->add(LogLevel::info(), 'Test message', [], $attributes); + + $logs = $aggregator->all(); + + $this->assertCount(1, $logs); + + $log = $logs[0]; + + $this->assertSame( + $expected, + array_filter( + $log->attributes()->toSimpleArray(), + static function (string $key) { + // We are not testing internal Sentry attributes here, only the ones the user supplied + return !str_starts_with($key, 'sentry.'); + }, + \ARRAY_FILTER_USE_KEY + ) + ); + } + + public static function attributesDataProvider(): \Generator + { + yield [ + [], + [], + ]; + + yield [ + ['foo', 'bar'], + [], + ]; + + yield [ + ['foo' => 'bar'], + ['foo' => 'bar'], + ]; + + yield [ + ['foo' => ['bar']], + [], + ]; + } + /** * @dataProvider messageFormattingDataProvider */ From 5669037f7768c72048cb99980207e46b7a5d27bf Mon Sep 17 00:00:00 2001 From: Michi Hoffmann Date: Wed, 20 Aug 2025 16:23:44 +0200 Subject: [PATCH 05/16] Prepare 4.15.0 (#1883) --- CHANGELOG.md | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d604d9e5dc..8c8efe2261 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,47 @@ # CHANGELOG +## 4.15.0 + +The Sentry SDK team is happy to announce the immediate availability of Sentry PHP SDK v4.15.0. + +### Features + +- Add Monolog Sentry Logs handler [(#1867)](https://github.com/getsentry/sentry-php/pull/1867) + + This new handler allows you to capture Monolog logs as Sentry logs. To use it, configure your Monolog logger: + + ```php + use Monolog\Logger; + use Sentry\Monolog\LogsHandler; + use Sentry\Logs\LogLevel; + + // Initialize Sentry SDK first (make sure 'enable_logs' is set to true) + \Sentry\init([ + 'dsn' => '__YOUR_DSN__', + 'enable_logs' => true, + ]); + + // Create a Monolog logger + $logger = new Logger('my-app'); + + // Add the Sentry logs handler + // Optional: specify minimum log level (defaults to LogLevel::debug()) + $handler = new LogsHandler(LogLevel::info()); + $logger->pushHandler($handler); + + // Now your logs will be sent to Sentry + $logger->info('User logged in', ['user_id' => 123]); + $logger->error('Payment failed', ['order_id' => 456]); + ``` + + Note: The handler will not collect logs for exceptions (they should be handled separately via `captureException`). + +### Bug Fixes + +- Fix non string indexed attributes passed as log attributes [(#1882)](https://github.com/getsentry/sentry-php/pull/1882) +- Use correct `sample_rate` key when deriving sampleRand [(#1874)](https://github.com/getsentry/sentry-php/pull/1874) +- Do not call `Reflection*::setAccessible()` in PHP >= 8.1 [(#1872)](https://github.com/getsentry/sentry-php/pull/1872) + ## 4.14.2 The Sentry SDK team is happy to announce the immediate availability of Sentry PHP SDK v4.14.2. From b2d84de69f3eda8ca22b0b00e9f923be3b837355 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Wed, 20 Aug 2025 14:26:37 +0000 Subject: [PATCH 06/16] release: 4.15.0 --- src/Client.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Client.php b/src/Client.php index 51d87bcb4c..6a8c0e9990 100644 --- a/src/Client.php +++ b/src/Client.php @@ -32,7 +32,7 @@ class Client implements ClientInterface /** * The version of the SDK. */ - public const SDK_VERSION = '4.14.2'; + public const SDK_VERSION = '4.15.0'; /** * Regex pattern to detect if a string is a regex pattern (starts and ends with / optionally followed by flags). From 9113a1bb4ed4ae29880c00acb6b8be9fd83ba3ae Mon Sep 17 00:00:00 2001 From: Alex Bouma Date: Thu, 28 Aug 2025 09:37:50 -0600 Subject: [PATCH 07/16] Do not send template attribute with logs when there are no template values (#1885) --- src/Logs/LogsAggregator.php | 9 ++++++--- tests/Logs/LogsAggregatorTest.php | 6 ++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/Logs/LogsAggregator.php b/src/Logs/LogsAggregator.php index ba1cc68b33..70b3715041 100644 --- a/src/Logs/LogsAggregator.php +++ b/src/Logs/LogsAggregator.php @@ -76,7 +76,6 @@ public function add( ->setAttribute('sentry.release', $options->getRelease()) ->setAttribute('sentry.environment', $options->getEnvironment() ?? Event::DEFAULT_ENVIRONMENT) ->setAttribute('sentry.server.address', $options->getServerName()) - ->setAttribute('sentry.message.template', $message) ->setAttribute('sentry.trace.parent_span_id', $hub->getSpan() ? $hub->getSpan()->getSpanId() : null); if ($client instanceof Client) { @@ -99,8 +98,12 @@ public function add( } }); - foreach ($values as $key => $value) { - $log->setAttribute("sentry.message.parameter.{$key}", $value); + if (\count($values)) { + $log->setAttribute('sentry.message.template', $message); + + foreach ($values as $key => $value) { + $log->setAttribute("sentry.message.parameter.{$key}", $value); + } } $attributes = Arr::simpleDot($attributes); diff --git a/tests/Logs/LogsAggregatorTest.php b/tests/Logs/LogsAggregatorTest.php index d60aaf5536..377b9deff8 100644 --- a/tests/Logs/LogsAggregatorTest.php +++ b/tests/Logs/LogsAggregatorTest.php @@ -105,6 +105,12 @@ public function testMessageFormatting(string $message, array $values, string $ex $log = $logs[0]; $this->assertSame($expected, $log->getBody()); + + if (\count($values)) { + $this->assertNotNull($log->attributes()->get('sentry.message.template')); + } else { + $this->assertNull($log->attributes()->get('sentry.message.template')); + } } public static function messageFormattingDataProvider(): \Generator From 32b97203beb56cb29325e975dc9a8893e5b554fd Mon Sep 17 00:00:00 2001 From: Michi Hoffmann Date: Thu, 28 Aug 2025 17:44:32 +0200 Subject: [PATCH 08/16] Prepare 4.15.1 (#1886) --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c8efe2261..d9d88599c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # CHANGELOG +## 4.15.1 + +The Sentry SDK team is happy to announce the immediate availability of Sentry PHP SDK v4.15.1. + +### Bug Fixes + +- Do not send `template` attribute with logs when there are no template values [(#1885)](https://github.com/getsentry/sentry-php/pull/1885) + ## 4.15.0 The Sentry SDK team is happy to announce the immediate availability of Sentry PHP SDK v4.15.0. From 0d09baf3700869ec4b723c95eb466de56c3d74b6 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Thu, 28 Aug 2025 15:45:14 +0000 Subject: [PATCH 09/16] release: 4.15.1 --- src/Client.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Client.php b/src/Client.php index 6a8c0e9990..fb08b44f0f 100644 --- a/src/Client.php +++ b/src/Client.php @@ -32,7 +32,7 @@ class Client implements ClientInterface /** * The version of the SDK. */ - public const SDK_VERSION = '4.15.0'; + public const SDK_VERSION = '4.15.1'; /** * Regex pattern to detect if a string is a regex pattern (starts and ends with / optionally followed by flags). From 042560059870dea42d2dd87e94a24d545ea267cb Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Tue, 2 Sep 2025 12:06:46 +0200 Subject: [PATCH 10/16] fix(logs): check if record should be handled within handle method (#1888) --- src/Monolog/LogsHandler.php | 3 + tests/Monolog/LogsHandlerTest.php | 110 +++++++++++++++++++++++++++++- 2 files changed, 110 insertions(+), 3 deletions(-) diff --git a/src/Monolog/LogsHandler.php b/src/Monolog/LogsHandler.php index 1d47fa90b6..08fc55e74b 100644 --- a/src/Monolog/LogsHandler.php +++ b/src/Monolog/LogsHandler.php @@ -54,6 +54,9 @@ public function isHandling($record): bool */ public function handle($record): bool { + if (!$this->isHandling($record)) { + return false; + } // Do not collect logs for exceptions, they should be handled seperately by the `Handler` or `captureException` if (isset($record['context']['exception']) && $record['context']['exception'] instanceof \Throwable) { return false; diff --git a/tests/Monolog/LogsHandlerTest.php b/tests/Monolog/LogsHandlerTest.php index 4abda1919b..9500fee59c 100644 --- a/tests/Monolog/LogsHandlerTest.php +++ b/tests/Monolog/LogsHandlerTest.php @@ -16,6 +16,11 @@ final class LogsHandlerTest extends TestCase { + protected function setUp(): void + { + Logs::getInstance()->flush(); + } + /** * @dataProvider handleDataProvider */ @@ -36,9 +41,6 @@ public function testHandle($record, Log $expectedLog): void $logs = Logs::getInstance()->aggregator()->all(); - // Clear the logs aggregator to avoid side effects in other tests - Logs::getInstance()->aggregator()->flush(); - $this->assertCount(1, $logs); $log = $logs[0]; @@ -58,6 +60,28 @@ static function (string $key) { ); } + /** + * @dataProvider logLevelDataProvider + */ + public function testLogLevels($record, int $countLogs): void + { + $client = ClientBuilder::create([ + 'enable_logs' => true, + 'before_send' => static function () { + return null; + }, + ])->getClient(); + + $hub = new Hub($client); + SentrySdk::setCurrentHub($hub); + + $handler = new LogsHandler(LogLevel::warn()); + $handler->handle($record); + + $logs = Logs::getInstance()->aggregator()->all(); + $this->assertCount($countLogs, $logs); + } + public static function handleDataProvider(): iterable { yield [ @@ -197,4 +221,84 @@ public static function handleDataProvider(): iterable ->setAttribute('bar', 'baz'), ]; } + + public static function logLevelDataProvider(): iterable + { + yield [ + RecordFactory::create( + 'foo bar', + Logger::DEBUG, + 'channel.foo', + [], + [] + ), + 0, + ]; + + yield [ + RecordFactory::create( + 'foo bar', + Logger::NOTICE, + 'channel.foo', + [], + [] + ), + 0, + ]; + + yield [ + RecordFactory::create( + 'foo bar', + Logger::INFO, + 'channel.foo', + [], + [] + ), + 0, + ]; + + yield [ + RecordFactory::create( + 'foo bar', + Logger::WARNING, + 'channel.foo', + [], + [] + ), + 1, + ]; + + yield [ + RecordFactory::create( + 'foo bar', + Logger::CRITICAL, + 'channel.foo', + [], + [] + ), + 1, + ]; + + yield [ + RecordFactory::create( + 'foo bar', + Logger::ALERT, + 'channel.foo', + [], + [] + ), + 1, + ]; + + yield [ + RecordFactory::create( + 'foo bar', + Logger::EMERGENCY, + 'channel.foo', + [], + [] + ), + 1, + ]; + } } From ea80724327632ae81420a60c372ad00c3b6dca34 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Wed, 3 Sep 2025 09:21:32 +0200 Subject: [PATCH 11/16] Prepare 4.15.2 (#1889) Co-authored-by: Michi Hoffmann --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9d88599c6..62db5cf854 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # CHANGELOG +## 4.15.2 + +The Sentry SDK team is happy to announce the immediate availability of Sentry PHP SDK v4.15.2. + +### Bug Fixes + +- Ensure the Monolog handler only processes records permitted by their log level. [(#1888)](https://github.com/getsentry/sentry-php/pull/1888) + ## 4.15.1 The Sentry SDK team is happy to announce the immediate availability of Sentry PHP SDK v4.15.1. From 61a2d918e8424b6de4a2e265c15133a00c17db51 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Wed, 3 Sep 2025 07:23:48 +0000 Subject: [PATCH 12/16] release: 4.15.2 --- src/Client.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Client.php b/src/Client.php index fb08b44f0f..3ca82713e5 100644 --- a/src/Client.php +++ b/src/Client.php @@ -32,7 +32,7 @@ class Client implements ClientInterface /** * The version of the SDK. */ - public const SDK_VERSION = '4.15.1'; + public const SDK_VERSION = '4.15.2'; /** * Regex pattern to detect if a string is a regex pattern (starts and ends with / optionally followed by flags). From 03b51d123502e7c29df14c3eb03c08ca67f3ac81 Mon Sep 17 00:00:00 2001 From: Felix Bernhard Date: Fri, 12 Sep 2025 12:06:36 +0200 Subject: [PATCH 13/16] Remove `max_breadcrumbs` limit (#1890) --- src/Options.php | 4 ++-- tests/OptionsTest.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Options.php b/src/Options.php index ea5e04787e..28093ed6e6 100644 --- a/src/Options.php +++ b/src/Options.php @@ -1457,13 +1457,13 @@ private function validateDsnOption($dsn): bool } /** - * Validates if the value of the max_breadcrumbs option is in range. + * Validates if the value of the max_breadcrumbs option is valid. * * @param int $value The value to validate */ private function validateMaxBreadcrumbsOptions(int $value): bool { - return $value >= 0 && $value <= self::DEFAULT_MAX_BREADCRUMBS; + return $value >= 0; } /** diff --git a/tests/OptionsTest.php b/tests/OptionsTest.php index 4192781a0f..4bc7c7fa40 100644 --- a/tests/OptionsTest.php +++ b/tests/OptionsTest.php @@ -586,7 +586,7 @@ public static function maxBreadcrumbsOptionIsValidatedCorrectlyDataProvider(): a [true, 0], [true, 1], [true, Options::DEFAULT_MAX_BREADCRUMBS], - [false, Options::DEFAULT_MAX_BREADCRUMBS + 1], + [true, Options::DEFAULT_MAX_BREADCRUMBS + 1], [false, 'string'], [false, '1'], ]; From 6e2e572540d97f30ec08775cecba6b7ff0d3cfa8 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Mon, 15 Sep 2025 10:47:37 +0200 Subject: [PATCH 14/16] rename CHANGELOG.md to CHANGELOG-5.0.md --- CHANGELOG.md => CHANGELOG-5.0.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename CHANGELOG.md => CHANGELOG-5.0.md (100%) diff --git a/CHANGELOG.md b/CHANGELOG-5.0.md similarity index 100% rename from CHANGELOG.md rename to CHANGELOG-5.0.md From 3155eeb2ddd156c92d5139e05b0096787b2ddc69 Mon Sep 17 00:00:00 2001 From: Michi Hoffmann Date: Mon, 15 Sep 2025 11:04:32 +0200 Subject: [PATCH 15/16] CI cleanup (#1901) --- .github/workflows/ci.yml | 2 +- .github/workflows/static-analysis.yaml | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8bc0b32936..6dee415f48 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -94,4 +94,4 @@ jobs: - name: Check benchmarks run: vendor/bin/phpbench run --revs=1 --iterations=1 - if: ${{ matrix.dependencies == 'highest' && matrix.php.version == '8.3' }} + if: ${{ matrix.dependencies == 'highest' && matrix.php.version == '8.4' }} diff --git a/.github/workflows/static-analysis.yaml b/.github/workflows/static-analysis.yaml index 8be436451c..d9e875a520 100644 --- a/.github/workflows/static-analysis.yaml +++ b/.github/workflows/static-analysis.yaml @@ -5,7 +5,7 @@ on: push: branches: - master - - develop + - release/** permissions: contents: read @@ -21,7 +21,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.3' + php-version: '8.4' - name: Install dependencies run: composer update --no-progress --no-interaction --prefer-dist @@ -39,7 +39,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.3' + php-version: '8.4' - name: Install dependencies run: composer update --no-progress --no-interaction --prefer-dist @@ -53,8 +53,6 @@ jobs: steps: - name: Checkout uses: actions/checkout@v5 - with: - fetch-depth: 2 # needed by codecov sometimes - name: Setup PHP uses: shivammathur/setup-php@v2 From 80692c6b0632b0e940b0fac810fdd9691a4173e1 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Mon, 15 Sep 2025 11:06:53 +0200 Subject: [PATCH 16/16] ref: remove @internal annotation from Result (#1904) Co-authored-by: Michi Hoffmann --- src/Transport/Result.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Transport/Result.php b/src/Transport/Result.php index a4a6b8238c..7d5061bc46 100644 --- a/src/Transport/Result.php +++ b/src/Transport/Result.php @@ -9,8 +9,6 @@ /** * This class contains the details of the sending operation of an event, e.g. * if it was sent successfully or if it was skipped because of some reason. - * - * @internal */ class Result {