From cd366d088e94ead85872a17fb6bc4d24dce727dc Mon Sep 17 00:00:00 2001 From: Krishan Koenig Date: Thu, 16 Jan 2025 14:37:47 +0100 Subject: [PATCH] create fallback issue --- src/Issues/Handler.php | 76 ++++++++++++++++++------------ tests/Issues/HandlerTest.php | 91 ++++++++++++++++++++++++------------ 2 files changed, 106 insertions(+), 61 deletions(-) diff --git a/src/Issues/Handler.php b/src/Issues/Handler.php index 9af97f9..7ebde0e 100644 --- a/src/Issues/Handler.php +++ b/src/Issues/Handler.php @@ -6,10 +6,13 @@ use Monolog\Handler\AbstractProcessingHandler; use Monolog\Level; use Monolog\LogRecord; +use Illuminate\Http\Client\PendingRequest; +use Illuminate\Http\Client\RequestException; class Handler extends AbstractProcessingHandler { private const DEFAULT_LABEL = 'github-issue-logger'; + private PendingRequest $client; /** * @param string $repo The GitHub repository in "owner/repo" format @@ -30,6 +33,7 @@ public function __construct( $this->repo = $repo; $this->token = $token; $this->labels = array_unique(array_merge([self::DEFAULT_LABEL], $labels)); + $this->client = Http::withToken($this->token)->baseUrl('https://api.github.com'); } /** @@ -38,19 +42,27 @@ public function __construct( protected function write(LogRecord $record): void { if (! $record->formatted instanceof Formatted) { - throw new \RuntimeException('Record must be formatted with '.Formatted::class); + throw new \RuntimeException('Record must be formatted with ' . Formatted::class); } $formatted = $record->formatted; - $existingIssue = $this->findExistingIssue($record); - if ($existingIssue) { - $this->commentOnIssue($existingIssue['number'], $formatted); + try { + $existingIssue = $this->findExistingIssue($record); - return; - } + if ($existingIssue) { + $this->commentOnIssue($existingIssue['number'], $formatted); + return; + } + + $this->createIssue($formatted); + } catch (RequestException $e) { + if ($e->response->serverError()) { + throw $e; + } - $this->createIssue($formatted); + $this->createFallbackIssue($formatted, $e->response->body()); + } } /** @@ -62,16 +74,12 @@ private function findExistingIssue(LogRecord $record): ?array throw new \RuntimeException('Record is missing github_issue_signature in extra data. Make sure the DeduplicationHandler is configured correctly.'); } - $response = Http::withToken($this->token) - ->get('https://api.github.com/search/issues', [ - 'q' => "repo:{$this->repo} is:issue is:open label:".self::DEFAULT_LABEL." \"Signature: {$record->extra['github_issue_signature']}\"", - ]); - - if ($response->failed()) { - throw new \RuntimeException('Failed to search GitHub issues: '.$response->body()); - } - - return $response->json('items.0', null); + return $this->client + ->get('/search/issues', [ + 'q' => "repo:{$this->repo} is:issue is:open label:" . self::DEFAULT_LABEL . " \"Signature: {$record->extra['github_issue_signature']}\"", + ]) + ->throw() + ->json('items.0', null); } /** @@ -79,14 +87,11 @@ private function findExistingIssue(LogRecord $record): ?array */ private function commentOnIssue(int $issueNumber, Formatted $formatted): void { - $response = Http::withToken($this->token) - ->post("https://api.github.com/repos/{$this->repo}/issues/{$issueNumber}/comments", [ + $this->client + ->post("/repos/{$this->repo}/issues/{$issueNumber}/comments", [ 'body' => $formatted->comment, - ]); - - if ($response->failed()) { - throw new \RuntimeException('Failed to comment on GitHub issue: '.$response->body()); - } + ]) + ->throw(); } /** @@ -94,15 +99,26 @@ private function commentOnIssue(int $issueNumber, Formatted $formatted): void */ private function createIssue(Formatted $formatted): void { - $response = Http::withToken($this->token) - ->post("https://api.github.com/repos/{$this->repo}/issues", [ + $this->client + ->post("/repos/{$this->repo}/issues", [ 'title' => $formatted->title, 'body' => $formatted->body, 'labels' => $this->labels, - ]); + ]) + ->throw(); + } - if ($response->failed()) { - throw new \RuntimeException('Failed to create GitHub issue: '.$response->body()); - } + /** + * Create a fallback issue when the main issue creation fails + */ + private function createFallbackIssue(Formatted $formatted, string $errorMessage): void + { + $this->client + ->post("/repos/{$this->repo}/issues", [ + 'title' => '[GitHub Monolog Error] ' . $formatted->title, + 'body' => "**Original Error Message:**\n{$formatted->body}\n\n**Integration Error:**\n{$errorMessage}", + 'labels' => array_merge($this->labels, ['monolog-integration-error']), + ]) + ->throw(); } } diff --git a/tests/Issues/HandlerTest.php b/tests/Issues/HandlerTest.php index 339319d..40751f3 100644 --- a/tests/Issues/HandlerTest.php +++ b/tests/Issues/HandlerTest.php @@ -2,6 +2,8 @@ namespace Tests\Issues; +use Illuminate\Http\Client\Request; +use Illuminate\Http\Client\RequestException; use Illuminate\Support\Facades\Http; use Monolog\Level; use Monolog\LogRecord; @@ -35,91 +37,118 @@ function createRecord(): LogRecord ); } -test('it creates new github issue when no duplicate exists', function () { - $handler = createHandler(); - $record = createRecord(); +beforeEach(function () { + Http::preventStrayRequests(); +}); +test('it creates new github issue when no duplicate exists', function () { Http::fake([ 'github.com/search/issues*' => Http::response(['items' => []]), 'github.com/repos/test/repo/issues' => Http::response(['number' => 1]), ]); + $handler = createHandler(); + $record = createRecord(); + $handler->handle($record); - Http::assertSent(function ($request) { - return str_contains($request->url(), '/repos/test/repo/issues') - && $request->method() === 'POST'; + Http::assertSent(function (Request $request) { + return str($request->url())->endsWith('/repos/test/repo/issues'); }); }); test('it comments on existing github issue', function () { - $handler = createHandler(); - $record = createRecord(); - Http::fake([ 'github.com/search/issues*' => Http::response(['items' => [['number' => 1]]]), 'github.com/repos/test/repo/issues/1/comments' => Http::response(['id' => 1]), ]); + $handler = createHandler(); + $record = createRecord(); + $handler->handle($record); Http::assertSent(function ($request) { - return str_contains($request->url(), '/issues/1/comments') - && $request->method() === 'POST'; + return str($request->url())->endsWith('/issues/1/comments'); }); }); test('it includes signature in issue search', function () { - $handler = createHandler(); - $record = createRecord(); - Http::fake([ 'github.com/search/issues*' => Http::response(['items' => []]), 'github.com/repos/test/repo/issues' => Http::response(['number' => 1]), ]); + $handler = createHandler(); + $record = createRecord(); + $handler->handle($record); Http::assertSent(function ($request) { - return str_contains($request->url(), '/search/issues') - && str_contains($request['q'], 'test-signature'); + return str($request->url())->contains('/search/issues') + && str_contains($request->data()['q'], 'test-signature'); }); }); test('it throws exception when issue search fails', function () { - $handler = createHandler(); - $record = createRecord(); - Http::fake([ 'github.com/search/issues*' => Http::response(['error' => 'Failed'], 500), ]); - expect(fn () => $handler->handle($record)) - ->toThrow('Failed to search GitHub issues'); -}); - -test('it throws exception when issue creation fails', function () { $handler = createHandler(); $record = createRecord(); + $handler->handle($record); +})->throws(RequestException::class, exceptionCode: 500); + +test('it throws exception when issue creation fails', function () { Http::fake([ 'github.com/search/issues*' => Http::response(['items' => []]), 'github.com/repos/test/repo/issues' => Http::response(['error' => 'Failed'], 500), ]); - expect(fn () => $handler->handle($record)) - ->toThrow('Failed to create GitHub issue'); -}); - -test('it throws exception when comment creation fails', function () { $handler = createHandler(); $record = createRecord(); + $handler->handle($record); +})->throws(RequestException::class, exceptionCode: 500); + +test('it throws exception when comment creation fails', function () { Http::fake([ 'github.com/search/issues*' => Http::response(['items' => [['number' => 1]]]), 'github.com/repos/test/repo/issues/1/comments' => Http::response(['error' => 'Failed'], 500), ]); - expect(fn () => $handler->handle($record)) - ->toThrow('Failed to comment on GitHub issue'); + $handler = createHandler(); + $record = createRecord(); + + $handler->handle($record); +})->throws(RequestException::class, exceptionCode: 500); + +test('it creates fallback issue when 4xx error occurs', function () { + $errorMessage = 'Validation failed for the issue'; + + Http::fake([ + 'github.com/search/issues*' => Http::response(['items' => []]), + 'github.com/repos/test/repo/issues' => Http::sequence() + ->push(['error' => $errorMessage], 422) + ->push(['number' => 1]), + ]); + + $handler = createHandler(); + $record = createRecord(); + + $handler->handle($record); + + Http::assertSent(function ($request) { + return str($request->url())->endsWith('/repos/test/repo/issues') + && !str_contains($request->data()['title'], '[GitHub Monolog Error]'); + }); + + Http::assertSent(function ($request) use ($errorMessage) { + return str($request->url())->endsWith('/repos/test/repo/issues') + && str_contains($request->data()['title'], '[GitHub Monolog Error]') + && str_contains($request->data()['body'], $errorMessage) + && in_array('monolog-integration-error', $request->data()['labels']); + }); });