Defensive fix: surface Google Analytics API 404 as a user error in keboola.ex-google-analytics-v4 - #132
Defensive fix: surface Google Analytics API 404 as a user error in keboola.ex-google-analytics-v4#132matyas-jirat-keboola wants to merge 2 commits into
Conversation
A 404 response from the Google Analytics API was not handled in Component::handleException() and fell through to ApplicationException, so the job ended with an opaque "internal error" (exit code 2) that told the user nothing and paged the team. Map 404 to UserException (exit code 1) with an actionable message, next to the existing 400/401/403/429/502/503 branches. The job still fails; only the way it reports the failure changes. Adds ComponentTest covering the new branch plus the neighbouring and fall-through status codes, so the existing mapping is pinned. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
@keboola-pr-reviewer review profile=component-factory |
keboola-pr-reviewer-bot
left a comment
There was a problem hiding this comment.
Verdict: auto_approve (risk 2/5) · profile component-factory
Auto-approve: additive 404 branch that fails more legibly without changing any working input's behaviour.
Concerns:
tests/Keboola/GoogleAnalyticsExtractor/ComponentTest.php: Test uses reflection/newInstanceWithoutConstructor; brittle to internal refactors.
There was a problem hiding this comment.
Pull request overview
This PR improves error classification in the Google Analytics extractor by treating Google Analytics Reporting API HTTP 404 responses as user-actionable failures (UserException) instead of opaque internal errors (ApplicationException), while leaving the happy path and existing status-code mappings unchanged.
Changes:
- Added explicit handling of HTTP 404 in
Component::handleException()to throwUserExceptionwith an actionable migration hint and the original API response body. - Added a new PHPUnit test suite validating the new 404 behavior and guarding that mapped/unmapped status codes keep their prior behavior.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
src/Component.php |
Adds a dedicated 404 branch in handleException() to surface the error as a UserException with user-facing guidance. |
tests/Keboola/GoogleAnalyticsExtractor/ComponentTest.php |
Introduces regression tests for 404 handling and invariants for other status codes’ exception mapping. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Addresses review feedback that the test was brittle to internal refactors. The test no longer reaches into BaseComponent's private $logger property by name, and no longer uses newInstanceWithoutConstructor(). It uses an anonymous subclass that skips the parent constructor and overrides the public getLogger(), so the only remaining reflection is invoking the private handleException() - which depends on this component's own code, not on a dependency's internals. Test-only change; src/ is untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
New commit on |
Dismissing prior approval — a new commit was pushed and this review was for an earlier SHA. Run @keboola-pr-reviewer-bot review to get a fresh verdict.
|
Thanks — addressed the one concern raised ( The test no longer reflects on $component = new class extends Component {
public function __construct()
{
// Intentionally does not call parent::__construct()
}
public function getLogger(): LoggerInterface
{
return new NullLogger();
}
};The only reflection left is invoking the private Verification after the change: the 9 new tests pass, Note on the red
|
|
@keboola-pr-reviewer review profile=component-factory |
keboola-pr-reviewer-bot
left a comment
There was a problem hiding this comment.
Verdict: needs_human (risk 4/5) · profile component-factory
Head commit has failing checks (Build); no required-check rules discovered on the base branch, falling back to block-on-any-failure.
Summary
Adds defensive handling for an unhandled
GuzzleHttp\Exception\ClientException(HTTP 404) observed in production. No change to the component's behaviour on inputs that already worked.Root cause
Component::handleException()maps a known set of HTTP status codes (400, 401, 403, 429, 502, 503) toUserException, and lets everything else fall through tothrow new ApplicationException(...)atsrc/Component.php:375.A 404 was never in that set. When the Google Analytics Reporting API responds:
the job ends as an opaque internal error (exit code 2) — the user is told nothing actionable, and the failure pages the team instead of the person who can fix it.
Classified as deterministic, user-actionable. In the alert window this accounted for 20 of 20 critical events for this component over 30 days, across 12+ distinct configurations and 2 stacks, spread evenly over 7 days — so it is a permanent condition, not a transient blip.
/v4/reports:batchGetis the Universal Analytics (view/profile) reporting endpoint; the GA4 property path uses a different host and is unaffected.Fix
Added one
404branch to the existing status-code chain inhandleException(), between the current403and502branches, that raisesUserExceptionwith an actionable message and the original API response body appended.Behaviour impact: none — defensive-only
returnwas added.ApplicationException— pinned by tests.Evidence
Datadog monitor: https://app.datadoghq.eu/monitors/97152903
Tests
New
tests/Keboola/GoogleAnalyticsExtractor/ComponentTest.php(9 tests, 13 assertions, green):testNotFoundIsSurfacedAsUserException— drives the new branch with the exact production response body and asserts theUserException, its code, message andgetPrevious(). Verified it fails onmasterwith the identical production error (ApplicationExceptionatsrc/Component.php:375), so it genuinely covers the fix.testUnmappedStatusCodesStillRaiseApplicationException(405, 409, 500) — guards the scope of the change: everything else still falls through exactly as before.testAlreadyMappedStatusCodesKeepRaisingUserException(400, 401, 429, 502, 503) — guards the neighbouring branches.parallel-lint,phpcsandphpstan --level=maxall clean.The credential-dependent test classes (
ApplicationTest,ClientTest,ExtractorTest,MigrateConfigurationTest) cannot run locally without live Google/Storage credentials and are left to CI. Baseline check on the credential-free classes: cleanmaster= 20 tests / 2 pre-existing credential failures; with this change = 29 tests / the same 2 failures. No new failures.