google/cloud-error-reporting
Idiomatic PHP client for Google Cloud Error Reporting (Stackdriver), which counts, analyzes, and aggregates crashes across your cloud services. Install via Composer; supports REST and gRPC. Beta component from Google Cloud PHP.
1. Install and pick a transport
composer require google/cloud-error-reporting
# optional, only if you want gRPC instead of REST over HTTP/1.1
pecl install grpc
The package ships generated GAPIC clients for the V1beta1 API: ErrorGroupServiceClient, ErrorStatsServiceClient, and ReportErrorsServiceClient, all under Google\Cloud\ErrorReporting\V1beta1\Client\. Use those \Client\ classes — the older non-Client classes were deprecated in v0.22 and removed in v0.26.
2. Authenticate before writing any code
Follow the standard Google Cloud PHP auth chain, not a bespoke one:
gcloud auth application-default login (ADC picks it up automatically).GOOGLE_APPLICATION_CREDENTIALS=/path/to/sa.json.The old credentials client option is deprecated (v0.24); stick with ADC or a credentials fetcher/callback rather than passing raw arrays around.
3. Understand the two things this package does — and which one you actually need
ReportErrorsServiceClient::reportErrorEvent() sends a ReportedErrorEvent (message + stack trace + ServiceContext) directly to Error Reporting. You almost never want this in a normal Laravel app.ErrorGroupServiceClient::getGroup() / updateGroup(), and ErrorStatsServiceClient::listGroupStats() / listEvents(). This is the 90% day-to-day use case — building an "errors" panel, a nightly digest command, or an ops dashboard.Most Laravel apps should report errors by writing structured logs to Cloud Logging (severity ERROR, plus serviceContext and a stack_trace) and let Error Reporting ingest them, then use this package only to query. That avoids a second network call in the request path and gives you the Cloud Console experience for free.
4. First working call — list top error groups for the last hour
use Google\Cloud\ErrorReporting\V1beta1\Client\ErrorStatsServiceClient;
use Google\Cloud\ErrorReporting\V1beta1\ListGroupStatsRequest;
use Google\Cloud\ErrorReporting\V1beta1\QueryTimeRange;
use Google\Cloud\ErrorReporting\V1beta1\QueryTimeRange\Period;
$client = new ErrorStatsServiceClient(); // ['transport' => 'rest'] if no grpc ext
$request = (new ListGroupStatsRequest())
->setProjectName(ErrorStatsServiceClient::projectName('my-gcp-project'))
->setTimeRange((new QueryTimeRange())->setPeriod(Period::PERIOD_1_HOUR));
foreach ($client->listGroupStats($request)->iterateAllElements() as $stat) {
printf("%s (%d)\n", $stat->getGroup()->getGroupId(), $stat->getCount());
}
5. First real use case to build: an artisan command (php artisan errors:digest) that pulls listGroupStats for your service for the last 24h and posts new groups to Slack. It is read-only, safe, cheap, and immediately proves the integration.
Route reporting through the exception handler, not controllers
Bind a small ErrorReporter interface in a service provider and call it from bootstrap/app.php / App\Exceptions\Handler::report():
public function register(): void
{
$this->app->singleton(ErrorReporter::class, fn ($app) => new CloudErrorReporter(
projectId: config('services.gcp.project'),
service: config('app.name'),
version: config('app.version', 'dev'),
));
}
Report in report(), not render() — render() runs for handled/4xx exceptions you usually don't want counted, and you'd double-report.
Prefer the log-based path and keep the SDK for queries
If you already use google/cloud-logging with Monolog, emit an entry shaped like the Error Reporting log format:
{
"severity": "ERROR",
"message": "Unhandled exception: …",
"serviceContext": { "service": "my-app", "version": "1.4.2" },
"@type": "type.googleapis.com/google.devtools.clouderrorreporting.v1beta1.ReportedErrorEvent",
"stack_trace": "…",
"context": { "httpRequest": { "method": "POST", "url": "/orders", "userAgent": "…" } }
}
context.httpRequest is generated/supported by this library (v0.21+). Then use ErrorStatsServiceClient only for stats. Pick one path per environment; running both produces duplicate groups and skewed counts.
Sample and buffer — never block the request on a Google API
random_int() or a config-driven rate.dispatchAfterResponse(), or the Laravel terminate() middleware so the client call happens after the response is flushed.Use the static name formatters
Every new-surface client exposes helpers such as ErrorStatsServiceClient::projectName($project) and ErrorGroupServiceClient::groupName($project, $groupId). Do not hand-build projects/…/groups/… strings; group IDs are opaque and easy to mangle.
Query patterns for dashboards
QueryTimeRange with a Period enum (PERIOD_1_HOUR, PERIOD_1_DAY, PERIOD_1_WEEK, PERIOD_30_DAYS) is the cheap path. Use explicit startTime/endTime only when you need a custom window — anything older than 30 days is gone.ServiceContextFilter (service, version) so multi-tenant apps don't blend services.iterateAllElements() for full pagination, or ->getPage()->getPageElementCount() plus getNextPageToken() when you need explicit control.listEvents, cap pageSize and use TimedCountAlignment / setPageSize() deliberately — this is the most expensive call in the API.Build an ops/tooling layer, not a user-facing feature
The realistic Laravel wins are: an /admin/errors page, an artisan digest, an on-call Slack alert job, and a "recent regressions after deploy" widget keyed on version from serviceContext. These map cleanly onto listGroupStats + getGroup.
Test against a fake, not the API
$this->app->instance(ErrorReporter::class, new FakeErrorReporter());
$this->assertCount(1, $fake->reported());
For client-level tests, Mockery::mock(ErrorStatsServiceClient::class) works because the class is not final in the generated surface. Don't let CI touch real GCP.
This is a 0.x beta package. Pin ^0.27 and read the release notes before bumping. v0.26 removed the deprecated GAPIC classes — a silent breaking change if you copy an older tutorial. composer why google/cloud-error-reporting on every major Laravel/PHP upgrade.
Reporting to the API ≠ appearing in Cloud Logging. reportErrorEvent() writes to Error Reporting's own store; it does not create log entries. Conversely, Error Reporting's automatic ingestion (Cloud Run, GAE, GKE) reads logs. Mixing the two gives you two different error counts in two different consoles. Choose consciously and document it.
Missing serviceContext.service lands everything under "unknown". This is the single most common reason dashboards look empty. Set service + version explicitly on every report/log entry, and use the same values Cloud Run/GKE would infer.
Never let monitoring break the app. Wrap every client call in try { } catch (\Throwable $e) { Log::warning(...); }. A Google\ApiCore\ApiException on a network blip inside Handler::report() will turn a 500 into a fatal. Also guard against recursion: if the logger or queue that reports errors itself throws, you get a loop.
Error grouping is stack-trace based. Small refactors, varying closure line numbers, or interpolated values in the message fragment groups badly. Keep messages stable and strip absolute paths / temp paths from stack_trace before reporting.
Redact before you send. ReportedErrorEvent payloads routinely capture PDO DSNs, bearer tokens, and user emails from exception messages. Sanitize in one place (a Sanitizer class) rather than per call site.
getGroup() throws NOT_FOUND for groups that were auto-deleted or aged out. Treat 404 as a normal branch, not an error — check $ex->getStatus() / getDetails() for diagnostics.
Transport and latency. gRPC is faster but requires the extension and adds cold-start cost on serverless (Bref, Lambda, Cloud Run scale-to-zero). Default to ['transport' => 'rest'] there, and set an explicit ['timeoutMillis' => 500] or requestTimeout — the default timeouts are far longer than you want in a request lifecycle.
The client has its own logger. Pass ['logger' => $psrLogger] in the client options to see actual RPCs, retries, and auth resolution. This is the fastest way to diagnose "it works locally, not in prod" — you'll usually see Could not load the default credentials or a project-ID mismatch.
Retries/backoff are configured via RetrySettings in the client options. Defaults are sane; only override if you're batching large listEvents queries, and add jitter when running many workers.
Local development: set ERROR_REPORTING_ENABLED=false in .env and bind a no-op reporter. Otherwise every dev-time exception hits your production project (or fails auth and slows the request).
Project ID resolution falls back to the metadata server / GOOGLE_CLOUD_PROJECT. If you run outside GCP, pass it explicitly in client options; a wrong project silently reports into a project you don't own.
Extension point: keep ErrorReporter as an interface with Cloud, Log-only, Sentry, and Fake implementations. It's the only way to swap vendors without touching Handler, and it keeps the beta status of this package from becoming an architectural dependency.
How can I help you explore Laravel packages today?