From 15ac54bcebc1aebd715630647a904f72b6215830 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Dec 2025 11:22:46 +0100 Subject: [PATCH 1/9] chore(deps): bump actions/create-github-app-token from 2.2.0 to 2.2.1 (#1986) 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 705c0958a..a1931d3bb 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@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0 + uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1 with: app-id: ${{ vars.SENTRY_RELEASE_BOT_CLIENT_ID }} private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }} From 5eea18c4d95172d2477c4079c6f1c30e3acfa898 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Tue, 13 Jan 2026 11:55:15 +0100 Subject: [PATCH 2/9] ref: add high level `flush` to flush all resources (#1989) --- src/SentrySdk.php | 18 ++++++++++++++++++ src/functions.php | 15 +++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/src/SentrySdk.php b/src/SentrySdk.php index 4b41fc30a..7621b44a6 100644 --- a/src/SentrySdk.php +++ b/src/SentrySdk.php @@ -4,6 +4,8 @@ namespace Sentry; +use Sentry\Logs\Logs; +use Sentry\Metrics\TraceMetrics; use Sentry\State\Hub; use Sentry\State\HubInterface; @@ -61,4 +63,20 @@ public static function setCurrentHub(HubInterface $hub): HubInterface return $hub; } + + /** + * Flushes all buffered telemetry data. + * + * This is a convenience facade that forwards the flush operation to all + * internally managed components. + * + * Calling this method is equivalent to invoking `flush()` on each component + * individually. It does not change flushing behavior, improve performance, + * or reduce the number of network requests. + */ + public static function flush(): void + { + Logs::getInstance()->flush(); + TraceMetrics::getInstance()->flush(); + } } diff --git a/src/functions.php b/src/functions.php index af967c8f9..2ff257151 100644 --- a/src/functions.php +++ b/src/functions.php @@ -396,3 +396,18 @@ function addFeatureFlag(string $name, bool $result): void $scope->addFeatureFlag($name, $result); }); } + +/** + * Flushes all buffered telemetry data. + * + * This is a convenience facade that forwards the flush operation to all + * internally managed components. + * + * Calling this method is equivalent to invoking `flush()` on each component + * individually. It does not change flushing behavior, improve performance, + * or reduce the number of network requests. + */ +function flush(): void +{ + SentrySdk::flush(); +} From b789493bcd9823f8064be0112910960ac0a7a26a Mon Sep 17 00:00:00 2001 From: Dawid 'DeyV' Polak Date: Tue, 13 Jan 2026 14:45:04 +0100 Subject: [PATCH 3/9] Update functions.php (#1994) --- src/functions.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/functions.php b/src/functions.php index 2ff257151..36c7c8b50 100644 --- a/src/functions.php +++ b/src/functions.php @@ -15,6 +15,7 @@ use Sentry\Tracing\SpanContext; use Sentry\Tracing\Transaction; use Sentry\Tracing\TransactionContext; +use Sentry\Transport\TransportInterface; /** * Creates a new Client and Hub which will be set as current. @@ -65,7 +66,7 @@ * trace_propagation_targets?: array|null, * traces_sample_rate?: float|int|null, * traces_sampler?: callable|null, - * transport?: callable, + * transport?: TransportInterface|null, * } $options The client options */ function init(array $options = []): void From f2f085b8640b9936c3368806978249b3c3b38d36 Mon Sep 17 00:00:00 2001 From: Philipp Hofmann Date: Tue, 20 Jan 2026 16:58:53 +0100 Subject: [PATCH 4/9] chore: ignore local Claude settings (#1997) --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index cd5863099..705a13d67 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ package.xml .phpunit.result.cache docs/_build tests/clover.xml + +# Local Claude Code settings that should not be committed +.claude/settings.local.json From 466d34089f9eac2e5a7cb02288a9736b3bc53b25 Mon Sep 17 00:00:00 2001 From: Michi Hoffmann Date: Mon, 2 Feb 2026 10:07:33 +0100 Subject: [PATCH 5/9] chore(ci): update PHPUnit versions in CI matrix (#2001) Co-authored-by: Claude Opus 4.5 Co-authored-by: Martin Linzmayer Co-authored-by: Alex Bouma --- .github/workflows/ci.yml | 18 ++++++++-------- .php-cs-fixer.dist.php | 1 + composer.json | 2 +- src/HttpClient/HttpClient.php | 2 +- src/Logs/LogsAggregator.php | 4 ++-- src/Metrics/Metrics.php | 2 +- src/Metrics/MetricsAggregator.php | 4 ++-- src/Tracing/Span.php | 2 +- src/functions.php | 10 ++++----- tests/ClientTest.php | 2 +- tests/FrameBuilderTest.php | 10 ++++----- tests/FunctionsTest.php | 20 ++++++++--------- tests/Logs/LogsAggregatorTest.php | 2 +- tests/Logs/LogsTest.php | 2 +- tests/Serializer/AbstractSerializerTest.php | 24 ++++++++++----------- tests/State/HubTest.php | 8 +++---- tests/State/ScopeTest.php | 4 ++-- tests/Transport/HttpTransportTest.php | 2 +- 18 files changed, 60 insertions(+), 59 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f1102d63d..bb51e01f2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,15 +27,15 @@ jobs: - ubuntu-latest - windows-latest php: - - { version: '7.2', phpunit: '^8.5.40' } - - { version: '7.3', phpunit: '^9.6.21' } - - { version: '7.4', phpunit: '^9.6.21' } - - { version: '8.0', phpunit: '^9.6.21' } - - { version: '8.1', phpunit: '^9.6.21' } - - { version: '8.2', phpunit: '^9.6.21' } - - { version: '8.3', phpunit: '^9.6.21' } - - { version: '8.4', phpunit: '^9.6.21' } - - { version: '8.5', phpunit: '^9.6.25' } + - { version: '7.2', phpunit: '^8.5.52' } + - { version: '7.3', phpunit: '^9.6.34' } + - { version: '7.4', phpunit: '^9.6.34' } + - { version: '8.0', phpunit: '^9.6.34' } + - { version: '8.1', phpunit: '^9.6.34' } + - { version: '8.2', phpunit: '^9.6.34' } + - { version: '8.3', phpunit: '^9.6.34' } + - { version: '8.4', phpunit: '^9.6.34' } + - { version: '8.5', phpunit: '^9.6.34' } dependencies: - lowest - highest diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 8a5c51fdb..aed65b5ed 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -38,6 +38,7 @@ ], 'no_whitespace_before_comma_in_array' => false, // Should be dropped when we drop support for PHP 7.x 'stringable_for_to_string' => false, + 'modern_serialization_methods' => false, // Could be re-enabled when we drop support for PHP 7.3 and lower ]) ->setRiskyAllowed(true) ->setFinder( diff --git a/composer.json b/composer.json index 607891141..28a1bd105 100644 --- a/composer.json +++ b/composer.json @@ -38,7 +38,7 @@ "monolog/monolog": "^1.6|^2.0|^3.0", "phpbench/phpbench": "^1.0", "phpstan/phpstan": "^1.3", - "phpunit/phpunit": "^8.5|^9.6", + "phpunit/phpunit": "^8.5.52|^9.6.34", "vimeo/psalm": "^4.17" }, "suggest": { diff --git a/src/HttpClient/HttpClient.php b/src/HttpClient/HttpClient.php index 2a373d930..fc0823373 100644 --- a/src/HttpClient/HttpClient.php +++ b/src/HttpClient/HttpClient.php @@ -57,7 +57,7 @@ public function sendRequest(Request $request, Options $options): Response } $responseHeaders = []; - $responseHeaderCallback = function ($curlHandle, $headerLine) use (&$responseHeaders): int { + $responseHeaderCallback = static function ($curlHandle, $headerLine) use (&$responseHeaders): int { return Http::parseResponseHeaders($headerLine, $responseHeaders); }; diff --git a/src/Logs/LogsAggregator.php b/src/Logs/LogsAggregator.php index e2fb5ea78..8d1ab7db4 100644 --- a/src/Logs/LogsAggregator.php +++ b/src/Logs/LogsAggregator.php @@ -83,7 +83,7 @@ public function add( $log->setAttribute('sentry.sdk.version', $client->getSdkVersion()); } - $hub->configureScope(function (Scope $scope) use ($log) { + $hub->configureScope(static function (Scope $scope) use ($log) { $user = $scope->getUser(); if ($user !== null) { if ($user->getId() !== null) { @@ -186,7 +186,7 @@ private function getTraceId(HubInterface $hub): string $traceId = ''; - $hub->configureScope(function (Scope $scope) use (&$traceId) { + $hub->configureScope(static function (Scope $scope) use (&$traceId) { $traceId = (string) $scope->getPropagationContext()->getTraceId(); }); diff --git a/src/Metrics/Metrics.php b/src/Metrics/Metrics.php index 936538e19..a45d7e6fc 100644 --- a/src/Metrics/Metrics.php +++ b/src/Metrics/Metrics.php @@ -109,7 +109,7 @@ public function timing( int $stackLevel = 0 ) { return trace( - function () use ($callback) { + static function () use ($callback) { return $callback(); }, SpanContext::make() diff --git a/src/Metrics/MetricsAggregator.php b/src/Metrics/MetricsAggregator.php index e18ca7ddf..a25df0b35 100644 --- a/src/Metrics/MetricsAggregator.php +++ b/src/Metrics/MetricsAggregator.php @@ -79,7 +79,7 @@ public function add( ]; if ($options->shouldSendDefaultPii()) { - $hub->configureScope(function (Scope $scope) use (&$defaultAttributes) { + $hub->configureScope(static function (Scope $scope) use (&$defaultAttributes) { $user = $scope->getUser(); if ($user !== null) { if ($user->getId() !== null) { @@ -111,7 +111,7 @@ public function add( $spanId = $span->getSpanId(); $traceId = $span->getTraceId(); } else { - $hub->configureScope(function (Scope $scope) use (&$traceId, &$spanId) { + $hub->configureScope(static function (Scope $scope) use (&$traceId, &$spanId) { $propagationContext = $scope->getPropagationContext(); $traceId = $propagationContext->getTraceId(); $spanId = $propagationContext->getSpanId(); diff --git a/src/Tracing/Span.php b/src/Tracing/Span.php index 09fe62a20..f45050620 100644 --- a/src/Tracing/Span.php +++ b/src/Tracing/Span.php @@ -300,7 +300,7 @@ public function setStatus(?SpanStatus $status) */ public function setHttpStatus(int $statusCode) { - SentrySdk::getCurrentHub()->configureScope(function (Scope $scope) use ($statusCode) { + SentrySdk::getCurrentHub()->configureScope(static function (Scope $scope) use ($statusCode) { $scope->setContext('response', [ 'status_code' => $statusCode, ]); diff --git a/src/functions.php b/src/functions.php index 36c7c8b50..a7412fce7 100644 --- a/src/functions.php +++ b/src/functions.php @@ -252,7 +252,7 @@ function startTransaction(TransactionContext $context, array $customSamplingCont */ function trace(callable $trace, SpanContext $context) { - return SentrySdk::getCurrentHub()->withScope(function (Scope $scope) use ($context, $trace) { + return SentrySdk::getCurrentHub()->withScope(static function (Scope $scope) use ($context, $trace) { $parentSpan = $scope->getSpan(); // If there is a span set on the scope and it's sampled there is an active transaction. @@ -299,7 +299,7 @@ function getTraceparent(): string } $traceParent = ''; - $hub->configureScope(function (Scope $scope) use (&$traceParent) { + $hub->configureScope(static function (Scope $scope) use (&$traceParent) { $traceParent = $scope->getPropagationContext()->toTraceparent(); }); @@ -342,7 +342,7 @@ function getBaggage(): string } $baggage = ''; - $hub->configureScope(function (Scope $scope) use (&$baggage) { + $hub->configureScope(static function (Scope $scope) use (&$baggage) { $baggage = $scope->getPropagationContext()->toBaggage(); }); @@ -358,7 +358,7 @@ function getBaggage(): string function continueTrace(string $sentryTrace, string $baggage): TransactionContext { $hub = SentrySdk::getCurrentHub(); - $hub->configureScope(function (Scope $scope) use ($sentryTrace, $baggage) { + $hub->configureScope(static function (Scope $scope) use ($sentryTrace, $baggage) { $propagationContext = PropagationContext::fromHeaders($sentryTrace, $baggage); $scope->setPropagationContext($propagationContext); }); @@ -393,7 +393,7 @@ function trace_metrics(): TraceMetrics */ function addFeatureFlag(string $name, bool $result): void { - SentrySdk::getCurrentHub()->configureScope(function (Scope $scope) use ($name, $result) { + SentrySdk::getCurrentHub()->configureScope(static function (Scope $scope) use ($name, $result) { $scope->addFeatureFlag($name, $result); }); } diff --git a/tests/ClientTest.php b/tests/ClientTest.php index a6cf67c8b..fb8b51d61 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -1033,7 +1033,7 @@ public function testAttachStacktrace(): void $transport = $this->createMock(TransportInterface::class); $transport->expects($this->once()) ->method('send') - ->with($this->callback(function (Event $event): bool { + ->with($this->callback(static function (Event $event): bool { $result = $event->getStacktrace(); return $result !== null; diff --git a/tests/FrameBuilderTest.php b/tests/FrameBuilderTest.php index 667b85a18..ecb01f0e1 100644 --- a/tests/FrameBuilderTest.php +++ b/tests/FrameBuilderTest.php @@ -290,7 +290,7 @@ public function testGetFunctionArgumentsWithVariadicParameters(): void $options = new Options([]); $frameBuilder = new FrameBuilder($options, new RepresentationSerializer($options)); - $testFunction = function (string $first, int $second, ...$rest) { + $testFunction = static function (string $first, int $second, ...$rest) { }; $backtraceFrame = [ @@ -325,7 +325,7 @@ public function testGetFunctionArgumentsWithOnlyVariadicParameters(): void $options = new Options([]); $frameBuilder = new FrameBuilder($options, new RepresentationSerializer($options)); - $testFunction = function (...$args) { + $testFunction = static function (...$args) { }; $backtraceFrame = [ @@ -352,7 +352,7 @@ public function testGetFunctionArgumentsWithEmptyVariadicParameters(): void $options = new Options([]); $frameBuilder = new FrameBuilder($options, new RepresentationSerializer($options)); - $testFunction = function (string $first, ...$rest) { + $testFunction = static function (string $first, ...$rest) { }; $backtraceFrame = [ @@ -381,7 +381,7 @@ public function testGetFunctionArgumentsWithNullValues(): void $options = new Options([]); $frameBuilder = new FrameBuilder($options, new RepresentationSerializer($options)); - $testFunction = function (string $first, $second, ...$rest) { + $testFunction = static function (string $first, $second, ...$rest) { }; $backtraceFrame = [ @@ -411,7 +411,7 @@ public function testGetFunctionArgumentsWithGapsInBacktraceArrayIndices(): void $options = new Options([]); $frameBuilder = new FrameBuilder($options, new RepresentationSerializer($options)); - $testFunction = function (string $first, int $second, ...$rest) { + $testFunction = static function (string $first, int $second, ...$rest) { }; $backtraceFrameArgs = []; diff --git a/tests/FunctionsTest.php b/tests/FunctionsTest.php index 949f9fadd..07e0bd918 100644 --- a/tests/FunctionsTest.php +++ b/tests/FunctionsTest.php @@ -224,15 +224,15 @@ public function testWithMonitor(): void $hub->expects($this->exactly(2)) ->method('captureCheckIn') ->with( - $this->callback(function (string $slug): bool { + $this->callback(static function (string $slug): bool { return $slug === 'test-crontab'; }), - $this->callback(function (CheckInStatus $checkInStatus): bool { + $this->callback(static function (CheckInStatus $checkInStatus): bool { // just check for type CheckInStatus return true; }), $this->anything(), - $this->callback(function (MonitorConfig $monitorConfig): bool { + $this->callback(static function (MonitorConfig $monitorConfig): bool { return $monitorConfig->getSchedule()->getValue() === '*/5 * * * *' && $monitorConfig->getSchedule()->getType() === MonitorSchedule::TYPE_CRONTAB && $monitorConfig->getCheckinMargin() === 5 @@ -243,7 +243,7 @@ public function testWithMonitor(): void SentrySdk::setCurrentHub($hub); - withMonitor('test-crontab', function () { + withMonitor('test-crontab', static function () { // Do something... }, new MonitorConfig( new MonitorSchedule(MonitorSchedule::TYPE_CRONTAB, '*/5 * * * *'), @@ -261,15 +261,15 @@ public function testWithMonitorCallableThrows(): void $hub->expects($this->exactly(2)) ->method('captureCheckIn') ->with( - $this->callback(function (string $slug): bool { + $this->callback(static function (string $slug): bool { return $slug === 'test-crontab'; }), - $this->callback(function (CheckInStatus $checkInStatus): bool { + $this->callback(static function (CheckInStatus $checkInStatus): bool { // just check for type CheckInStatus return true; }), $this->anything(), - $this->callback(function (MonitorConfig $monitorConfig): bool { + $this->callback(static function (MonitorConfig $monitorConfig): bool { return $monitorConfig->getSchedule()->getValue() === '*/5 * * * *' && $monitorConfig->getSchedule()->getType() === MonitorSchedule::TYPE_CRONTAB && $monitorConfig->getCheckinMargin() === 5 @@ -280,7 +280,7 @@ public function testWithMonitorCallableThrows(): void SentrySdk::setCurrentHub($hub); - withMonitor('test-crontab', function () { + withMonitor('test-crontab', static function () { throw new \Exception(); }, new MonitorConfig( new MonitorSchedule(MonitorSchedule::TYPE_CRONTAB, '*/5 * * * *'), @@ -352,7 +352,7 @@ public function testTraceReturnsClosureResult(): void { $returnValue = 'foo'; - $result = trace(function () use ($returnValue) { + $result = trace(static function () use ($returnValue) { return $returnValue; }, new SpanContext()); @@ -379,7 +379,7 @@ public function testTraceCorrectlyReplacesAndRestoresCurrentSpan(): void $this->assertSame($transaction, $hub->getSpan()); try { - trace(function () { + trace(static function () { throw new \RuntimeException('Throwing should still restore the previous span'); }, new SpanContext()); } catch (\RuntimeException $e) { diff --git a/tests/Logs/LogsAggregatorTest.php b/tests/Logs/LogsAggregatorTest.php index 83a91e1da..89e175a70 100644 --- a/tests/Logs/LogsAggregatorTest.php +++ b/tests/Logs/LogsAggregatorTest.php @@ -170,7 +170,7 @@ public function testAttributesAreAddedToLogMessage(): void $hub = new Hub($client); SentrySdk::setCurrentHub($hub); - $hub->configureScope(function (Scope $scope) { + $hub->configureScope(static function (Scope $scope) { $userDataBag = new UserDataBag(); $userDataBag->setId('unique_id'); $userDataBag->setEmail('foo@example.com'); diff --git a/tests/Logs/LogsTest.php b/tests/Logs/LogsTest.php index 3aab97419..4dd361b56 100644 --- a/tests/Logs/LogsTest.php +++ b/tests/Logs/LogsTest.php @@ -171,7 +171,7 @@ private function assertEvent(callable $assert, array $options = []): ClientInter $transport = $this->createMock(TransportInterface::class); $transport->expects($this->once()) ->method('send') - ->with($this->callback(function (Event $event) use ($assert): bool { + ->with($this->callback(static function (Event $event) use ($assert): bool { $assert($event); return true; diff --git a/tests/Serializer/AbstractSerializerTest.php b/tests/Serializer/AbstractSerializerTest.php index 36f753c73..89932ba1a 100644 --- a/tests/Serializer/AbstractSerializerTest.php +++ b/tests/Serializer/AbstractSerializerTest.php @@ -458,7 +458,7 @@ public function serializableCallableProvider(): array return [ [ - 'callable' => function (array $param1) { + 'callable' => static function (array $param1) { throw new \Exception('Don\'t even think about invoke me'); }, 'expected' => $prettyClosureNames @@ -466,7 +466,7 @@ public function serializableCallableProvider(): array : 'Lambda ' . __NAMESPACE__ . '\\{closure} [array param1]', ], [ - 'callable' => function ($param1a) { + 'callable' => static function ($param1a) { throw new \Exception('Don\'t even think about invoke me'); }, 'expected' => $prettyClosureNames @@ -474,7 +474,7 @@ public function serializableCallableProvider(): array : 'Lambda ' . __NAMESPACE__ . '\\{closure} [mixed|null param1a]', ], [ - 'callable' => function (callable $param1c) { + 'callable' => static function (callable $param1c) { throw new \Exception('Don\'t even think about invoke me'); }, 'expected' => $prettyClosureNames @@ -482,7 +482,7 @@ public function serializableCallableProvider(): array : 'Lambda ' . __NAMESPACE__ . '\\{closure} [callable param1c]', ], [ - 'callable' => function (\stdClass $param1d) { + 'callable' => static function (\stdClass $param1d) { throw new \Exception('Don\'t even think about invoke me'); }, 'expected' => $prettyClosureNames @@ -490,7 +490,7 @@ public function serializableCallableProvider(): array : 'Lambda ' . __NAMESPACE__ . '\\{closure} [stdClass param1d]', ], [ - 'callable' => function (?\stdClass $param1e = null) { + 'callable' => static function (?\stdClass $param1e = null) { throw new \Exception('Don\'t even think about invoke me'); }, 'expected' => $prettyClosureNames @@ -498,7 +498,7 @@ public function serializableCallableProvider(): array : 'Lambda ' . __NAMESPACE__ . '\\{closure} [stdClass|null [param1e]]', ], [ - 'callable' => function (array &$param1f) { + 'callable' => static function (array &$param1f) { throw new \Exception('Don\'t even think about invoke me'); }, 'expected' => $prettyClosureNames @@ -506,7 +506,7 @@ public function serializableCallableProvider(): array : 'Lambda ' . __NAMESPACE__ . '\\{closure} [array ¶m1f]', ], [ - 'callable' => function (?array &$param1g = null) { + 'callable' => static function (?array &$param1g = null) { throw new \Exception('Don\'t even think about invoke me'); }, 'expected' => $prettyClosureNames @@ -534,7 +534,7 @@ public function serializableCallableProvider(): array 'expected' => 'Callable void ' . SerializerTestObject::class . '::testy []', ], [ - 'callable' => function (int $param1_70a) { + 'callable' => static function (int $param1_70a) { throw new \Exception('Don\'t even think about invoke me'); }, 'expected' => $prettyClosureNames @@ -542,7 +542,7 @@ public function serializableCallableProvider(): array : 'Lambda ' . __NAMESPACE__ . '\\{closure} [int param1_70a]', ], [ - 'callable' => function (&$param): int { + 'callable' => static function (&$param): int { return (int) $param; }, 'expected' => $prettyClosureNames @@ -550,7 +550,7 @@ public function serializableCallableProvider(): array : 'Lambda int ' . __NAMESPACE__ . '\\{closure} [mixed|null ¶m]', ], [ - 'callable' => function (int $param): ?int { + 'callable' => static function (int $param): ?int { throw new \Exception('Don\'t even think about invoke me'); }, 'expected' => $prettyClosureNames @@ -558,7 +558,7 @@ public function serializableCallableProvider(): array : 'Lambda int ' . __NAMESPACE__ . '\\{closure} [int param]', ], [ - 'callable' => function (?int $param1_70b) { + 'callable' => static function (?int $param1_70b) { throw new \Exception('Don\'t even think about invoke me'); }, 'expected' => $prettyClosureNames @@ -566,7 +566,7 @@ public function serializableCallableProvider(): array : 'Lambda ' . __NAMESPACE__ . '\\{closure} [int|null param1_70b]', ], [ - 'callable' => function (?int $param1_70c): void { + 'callable' => static function (?int $param1_70c): void { throw new \Exception('Don\'t even think about invoke me'); }, 'expected' => $prettyClosureNames diff --git a/tests/State/HubTest.php b/tests/State/HubTest.php index bb735cb96..0fb31e2cd 100644 --- a/tests/State/HubTest.php +++ b/tests/State/HubTest.php @@ -240,7 +240,7 @@ public function testCaptureMessage(array $functionCallArgs, array $expectedFunct $eventId = EventId::generate(); $hub = new Hub(); - $hub->configureScope(function (Scope $scope) use ($propagationContext): void { + $hub->configureScope(static function (Scope $scope) use ($propagationContext): void { $scope->setPropagationContext($propagationContext); }); @@ -299,7 +299,7 @@ public function testCaptureException(array $functionCallArgs, array $expectedFun $eventId = EventId::generate(); $hub = new Hub(); - $hub->configureScope(function (Scope $scope) use ($propagationContext): void { + $hub->configureScope(static function (Scope $scope) use ($propagationContext): void { $scope->setPropagationContext($propagationContext); }); @@ -354,7 +354,7 @@ public function testCaptureLastError(array $functionCallArgs, array $expectedFun $eventId = EventId::generate(); $hub = new Hub(); - $hub->configureScope(function (Scope $scope) use ($propagationContext): void { + $hub->configureScope(static function (Scope $scope) use ($propagationContext): void { $scope->setPropagationContext($propagationContext); }); @@ -813,7 +813,7 @@ public function testStartTransactionUpdatesTheDscSampleRate(): void $client->expects($this->once()) ->method('getOptions') ->willReturn(new Options([ - 'traces_sampler' => function (SamplingContext $samplingContext): float { + 'traces_sampler' => static function (SamplingContext $samplingContext): float { return 1.0; }, ])); diff --git a/tests/State/ScopeTest.php b/tests/State/ScopeTest.php index 281cbadb9..f2d99db01 100644 --- a/tests/State/ScopeTest.php +++ b/tests/State/ScopeTest.php @@ -405,7 +405,7 @@ public function testAddEventProcessor(): void return null; }); - $scope->addEventProcessor(function () use (&$callback3Called) { + $scope->addEventProcessor(static function () use (&$callback3Called) { $callback3Called = true; return null; @@ -425,7 +425,7 @@ public function testEventProcessorReceivesTheEventAndEventHint(): void $processorCalled = false; $processorReceivedHint = null; - $scope->addEventProcessor(function (Event $eventArg, EventHint $hint) use (&$processorCalled, &$processorReceivedHint): ?Event { + $scope->addEventProcessor(static function (Event $eventArg, EventHint $hint) use (&$processorCalled, &$processorReceivedHint): ?Event { $processorCalled = true; $processorReceivedHint = $hint; diff --git a/tests/Transport/HttpTransportTest.php b/tests/Transport/HttpTransportTest.php index 1d34137ae..206261eb0 100644 --- a/tests/Transport/HttpTransportTest.php +++ b/tests/Transport/HttpTransportTest.php @@ -62,7 +62,7 @@ public function testSend(Response $response, ResultStatus $expectedResultStatus, $this->logger->expects($this->exactly(\count($messages))) ->method($level) ->with($this->logicalOr( - ...array_map(function (string $message) { + ...array_map(static function (string $message) { return new StringMatchesFormatDescription($message); }, $messages) )); From 5c38fd2ce14642b0f3e1f322ea3e99da0f255dc7 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Thu, 12 Feb 2026 14:53:10 +0000 Subject: [PATCH 6/9] ci(release): Switch from action-prepare-release to Craft (#1993) --- .github/workflows/changelog-preview.yml | 18 ++++++++++++++++++ .github/workflows/publish-release.yaml | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/changelog-preview.yml diff --git a/.github/workflows/changelog-preview.yml b/.github/workflows/changelog-preview.yml new file mode 100644 index 000000000..30c6083c6 --- /dev/null +++ b/.github/workflows/changelog-preview.yml @@ -0,0 +1,18 @@ +name: Changelog Preview +on: + pull_request: + types: + - opened + - synchronize + - reopened + - edited + - labeled + - unlabeled +permissions: + contents: write + pull-requests: write + +jobs: + changelog-preview: + uses: getsentry/craft/.github/workflows/changelog-preview.yml@v2 + secrets: inherit diff --git a/.github/workflows/publish-release.yaml b/.github/workflows/publish-release.yaml index a1931d3bb..bff6fefe1 100644 --- a/.github/workflows/publish-release.yaml +++ b/.github/workflows/publish-release.yaml @@ -35,7 +35,7 @@ jobs: fetch-depth: 0 - name: Prepare release - uses: getsentry/action-prepare-release@v1 + uses: getsentry/craft@c6e2f04939b6ee67030588afbb5af76b127d8203 env: GITHUB_TOKEN: ${{ steps.token.outputs.token }} with: From ba72c2940d5c335376d366c2fef13b653d280d85 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Thu, 12 Feb 2026 20:23:49 +0530 Subject: [PATCH 7/9] ref(logs): allow different log level types (#1992) --- src/Monolog/LogsHandler.php | 23 ++- tests/Monolog/LogsHandlerTest.php | 229 ++++++++++++++++++------------ tests/Monolog/RecordFactory.php | 2 +- 3 files changed, 157 insertions(+), 97 deletions(-) diff --git a/src/Monolog/LogsHandler.php b/src/Monolog/LogsHandler.php index 9ee342a4f..b92640b14 100644 --- a/src/Monolog/LogsHandler.php +++ b/src/Monolog/LogsHandler.php @@ -18,7 +18,9 @@ class LogsHandler implements HandlerInterface /** * The minimum logging level at which this handler will be triggered. * - * @var LogLevel + * @psalm-suppress UndefinedDocblockClass + * + * @var LogLevel|\Monolog\Level|int */ private $logLevel; @@ -32,21 +34,32 @@ class LogsHandler implements HandlerInterface /** * Creates a new Monolog handler that converts Monolog logs to Sentry logs. * - * @param LogLevel|null $logLevel the minimum logging level at which this handler will be triggered and collects the logs - * @param bool $bubble whether the messages that are handled can bubble up the stack or not + * @psalm-suppress UndefinedDocblockClass + * + * @param LogLevel|\Monolog\Level|int|null $logLevel the minimum logging level at which this handler will be triggered and collects the logs + * @param bool $bubble whether the messages that are handled can bubble up the stack or not */ - public function __construct(?LogLevel $logLevel = null, bool $bubble = true) + public function __construct($logLevel = null, bool $bubble = true) { $this->logLevel = $logLevel ?? LogLevel::debug(); $this->bubble = $bubble; } /** + * @psalm-suppress UndefinedDocblockClass + * @psalm-suppress UndefinedClass + * * @param array|LogRecord $record */ public function isHandling($record): bool { - return self::getSentryLogLevelFromMonologLevel($record['level'])->getPriority() >= $this->logLevel->getPriority(); + if ($this->logLevel instanceof LogLevel) { + return self::getSentryLogLevelFromMonologLevel($record['level'])->getPriority() >= $this->logLevel->getPriority(); + } elseif ($this->logLevel instanceof \Monolog\Level) { + return $record['level'] >= $this->logLevel->value; + } else { + return $record['level'] >= $this->logLevel; + } } /** diff --git a/tests/Monolog/LogsHandlerTest.php b/tests/Monolog/LogsHandlerTest.php index 2a6af3298..004a80c38 100644 --- a/tests/Monolog/LogsHandlerTest.php +++ b/tests/Monolog/LogsHandlerTest.php @@ -4,19 +4,17 @@ namespace Sentry\Tests\Monolog; +use Monolog\Level; use Monolog\Logger; use PHPUnit\Framework\TestCase; use Sentry\ClientBuilder; -use Sentry\Event; use Sentry\Logs\Log; use Sentry\Logs\LogLevel; use Sentry\Logs\Logs; use Sentry\Monolog\LogsHandler; use Sentry\SentrySdk; use Sentry\State\Hub; -use Sentry\Transport\Result; -use Sentry\Transport\ResultStatus; -use Sentry\Transport\TransportInterface; +use Sentry\Tests\StubTransport; final class LogsHandlerTest extends TestCase { @@ -64,42 +62,47 @@ static function (string $key) { } /** - * @dataProvider logLevelDataProvider + * @dataProvider monologLegacyLevelDataProvider */ - public function testLogLevels($record, int $countLogs): void + public function testFiltersAndMapsUsingLegacyMonologThreshold(int $threshold, int $recordLevel, int $expectedCount, ?LogLevel $expectedMappedLevel): void { - $handler = new LogsHandler(LogLevel::warn()); - $handler->handle($record); + $handler = new LogsHandler($threshold); + $handler->handle(RecordFactory::create('foo bar', $recordLevel, 'channel.foo', [], [])); $logs = Logs::getInstance()->aggregator()->all(); - $this->assertCount($countLogs, $logs); + $this->assertCount($expectedCount, $logs); + + if ($expectedMappedLevel !== null) { + $this->assertEquals($expectedMappedLevel, $logs[0]->getLevel()); + } + } + + /** + * @dataProvider monologLevelDataProvider + */ + public function testFiltersAndMapsUsingMonologEnumThreshold($threshold, $recordLevel, int $expectedCount, ?LogLevel $expectedMappedLevel): void + { + if (!class_exists(Level::class)) { + $this->markTestSkipped('Test only works for Monolog >= 3'); + } + + $this->assertInstanceOf(Level::class, $threshold); + $this->assertInstanceOf(Level::class, $recordLevel); + + $handler = new LogsHandler($threshold); + $handler->handle(RecordFactory::create('foo bar', $recordLevel->value, 'channel.foo', [], [])); + + $logs = Logs::getInstance()->aggregator()->all(); + $this->assertCount($expectedCount, $logs); + + if ($expectedMappedLevel !== null) { + $this->assertEquals($expectedMappedLevel, $logs[0]->getLevel()); + } } public function testLogsHandlerDestructor() { - $transport = new class implements TransportInterface { - private $events = []; - - public function send(Event $event): Result - { - $this->events[] = $event; - - return new Result(ResultStatus::success()); - } - - public function close(?int $timeout = null): Result - { - return new Result(ResultStatus::success()); - } - - /** - * @return Event[] - */ - public function getEvents(): array - { - return $this->events; - } - }; + $transport = new StubTransport(); $client = ClientBuilder::create([ 'enable_logs' => true, ])->setTransport($transport) @@ -110,8 +113,8 @@ public function getEvents(): array $this->handleLogAndDrop(); - $this->assertCount(1, $transport->getEvents()); - $this->assertSame('I was dropped :(', $transport->getEvents()[0]->getLogs()[0]->getBody()); + $this->assertCount(1, StubTransport::$events); + $this->assertSame('I was dropped :(', StubTransport::$events[0]->getLogs()[0]->getBody()); } private function handleLogAndDrop(): void @@ -283,83 +286,127 @@ public static function handleDataProvider(): iterable ]; } - public static function logLevelDataProvider(): iterable + public static function monologLegacyLevelDataProvider(): iterable { - yield [ - RecordFactory::create( - 'foo bar', - Logger::DEBUG, - 'channel.foo', - [], - [] - ), + yield 'NOTICE threshold drops INFO (both map to sentry info)' => [ + Logger::NOTICE, + Logger::INFO, 0, + null, ]; - yield [ - RecordFactory::create( - 'foo bar', - Logger::NOTICE, - 'channel.foo', - [], - [] - ), + yield 'NOTICE threshold keeps NOTICE (mapped to sentry info)' => [ + Logger::NOTICE, + Logger::NOTICE, + 1, + LogLevel::info(), + ]; + + yield 'NOTICE threshold keeps WARNING (mapped to sentry warn)' => [ + Logger::NOTICE, + Logger::WARNING, + 1, + LogLevel::warn(), + ]; + + yield 'ALERT threshold drops CRITICAL (both map to sentry fatal)' => [ + Logger::ALERT, + Logger::CRITICAL, 0, + null, ]; - yield [ - RecordFactory::create( - 'foo bar', - Logger::INFO, - 'channel.foo', - [], - [] - ), + yield 'ALERT threshold keeps ALERT (mapped to sentry fatal)' => [ + Logger::ALERT, + Logger::ALERT, + 1, + LogLevel::fatal(), + ]; + + yield 'ALERT threshold keeps EMERGENCY (mapped to sentry fatal)' => [ + Logger::ALERT, + Logger::EMERGENCY, + 1, + LogLevel::fatal(), + ]; + + yield 'EMERGENCY threshold drops ALERT (both map to sentry fatal)' => [ + Logger::EMERGENCY, + Logger::ALERT, 0, + null, ]; - yield [ - RecordFactory::create( - 'foo bar', - Logger::WARNING, - 'channel.foo', - [], - [] - ), + yield 'EMERGENCY threshold keeps EMERGENCY (mapped to sentry fatal)' => [ + Logger::EMERGENCY, + Logger::EMERGENCY, 1, + LogLevel::fatal(), ]; + } - yield [ - RecordFactory::create( - 'foo bar', - Logger::CRITICAL, - 'channel.foo', - [], - [] - ), + public static function monologLevelDataProvider(): iterable + { + if (!class_exists(Level::class)) { + yield 'Monolog < 3 (skipped)' => [null, null, 0, null]; + + return; + } + + yield 'NOTICE threshold drops INFO (both map to sentry info)' => [ + Level::Notice, + Level::Info, + 0, + null, + ]; + + yield 'NOTICE threshold keeps NOTICE (mapped to sentry info)' => [ + Level::Notice, + Level::Notice, 1, + LogLevel::info(), ]; - yield [ - RecordFactory::create( - 'foo bar', - Logger::ALERT, - 'channel.foo', - [], - [] - ), + yield 'NOTICE threshold keeps WARNING (mapped to sentry warn)' => [ + Level::Notice, + Level::Warning, 1, + LogLevel::warn(), ]; - yield [ - RecordFactory::create( - 'foo bar', - Logger::EMERGENCY, - 'channel.foo', - [], - [] - ), + yield 'ALERT threshold drops CRITICAL (both map to sentry fatal)' => [ + Level::Alert, + Level::Critical, + 0, + null, + ]; + + yield 'ALERT threshold keeps ALERT (mapped to sentry fatal)' => [ + Level::Alert, + Level::Alert, + 1, + LogLevel::fatal(), + ]; + + yield 'ALERT threshold keeps EMERGENCY (mapped to sentry fatal)' => [ + Level::Alert, + Level::Emergency, + 1, + LogLevel::fatal(), + ]; + + yield 'EMERGENCY threshold drops ALERT (both map to sentry fatal)' => [ + Level::Emergency, + Level::Alert, + 0, + null, + ]; + + yield 'EMERGENCY threshold keeps EMERGENCY (mapped to sentry fatal)' => [ + Level::Emergency, + Level::Emergency, 1, + LogLevel::fatal(), ]; } } diff --git a/tests/Monolog/RecordFactory.php b/tests/Monolog/RecordFactory.php index 39e2b67d6..be2213059 100644 --- a/tests/Monolog/RecordFactory.php +++ b/tests/Monolog/RecordFactory.php @@ -19,7 +19,7 @@ final class RecordFactory * * @return array|LogRecord */ - public static function create(string $message, int $level, string $channel, array $context, array $extra) + public static function create(string $message, int $level, string $channel, array $context = [], array $extra = []) { if (Logger::API >= 3) { return new LogRecord( From 1904b902aefdaa7e59258df0c6824c541d05e5f5 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Thu, 12 Feb 2026 20:48:07 +0530 Subject: [PATCH 8/9] chore: fixes CS due to new rule (#2005) --- src/Monolog/LogsHandler.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Monolog/LogsHandler.php b/src/Monolog/LogsHandler.php index b92640b14..9eb49e440 100644 --- a/src/Monolog/LogsHandler.php +++ b/src/Monolog/LogsHandler.php @@ -57,9 +57,9 @@ public function isHandling($record): bool return self::getSentryLogLevelFromMonologLevel($record['level'])->getPriority() >= $this->logLevel->getPriority(); } elseif ($this->logLevel instanceof \Monolog\Level) { return $record['level'] >= $this->logLevel->value; - } else { - return $record['level'] >= $this->logLevel; } + + return $record['level'] >= $this->logLevel; } /** From 0e2451767a7968843e39111e1cd2b2f597f53b23 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Thu, 12 Feb 2026 16:29:14 +0100 Subject: [PATCH 9/9] CS --- tests/OptionResolverTest.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/OptionResolverTest.php b/tests/OptionResolverTest.php index df8d9c470..176b2dd17 100644 --- a/tests/OptionResolverTest.php +++ b/tests/OptionResolverTest.php @@ -114,7 +114,7 @@ public function testNormalizerReturnsInvalidType() $resolver = new OptionsResolver(); $resolver->setDefaults(['foo' => 'bar']); $resolver->setAllowedTypes('foo', ['string']); - $resolver->setNormalizer('foo', function ($value) { + $resolver->setNormalizer('foo', static function ($value) { return 8; }); $result = $resolver->resolve(['foo' => 'test']); @@ -126,7 +126,7 @@ public function testNormalizerReturnsInvalidValue() $resolver = new OptionsResolver(); $resolver->setDefaults(['foo' => 'b']); $resolver->setAllowedValues('foo', ['a', 'b', 'c']); - $resolver->setNormalizer('foo', function ($value) { + $resolver->setNormalizer('foo', static function ($value) { return 'z'; }); $result = $resolver->resolve(['foo' => 'a']); @@ -138,7 +138,7 @@ public function testNormalizerResultFailsValidation() $resolver = new OptionsResolver(); $resolver->setDefaults(['foo' => 'b']); $resolver->setAllowedValues('foo', ['a', 'b', 'c']); - $resolver->setNormalizer('foo', function ($value) { + $resolver->setNormalizer('foo', static function ($value) { return false; }); $result = $resolver->resolve(['foo' => 'a']); @@ -237,7 +237,7 @@ public function allowedValueTestProvider(): \Generator yield 'Callback validates successfully' => [ ['count' => 50], ['count' => 10], - ['count' => function ($value) { + ['count' => static function ($value) { return $value >= 0 && $value <= 100; }], ['count' => 10], @@ -246,7 +246,7 @@ public function allowedValueTestProvider(): \Generator yield 'Callback validation fails' => [ ['count' => 50], ['count' => 200], - ['count' => function ($value) { + ['count' => static function ($value) { return $value >= 0 && $value <= 100; }], ['count' => 50], @@ -281,7 +281,7 @@ public function normalizerTestProvider(): \Generator yield 'Normalizes successful' => [ ['a' => 'b'], ['a' => ' c '], - ['a' => function ($value) { + ['a' => static function ($value) { return trim($value); }], ['a' => 'c'],