Looking to hire Laravel developers? Try LaraJobs

laravel-issue-reporter maintained by asdrubalp9

Description
Capture Laravel errors and report them as GitLab issues, with deduplication.
Author
Last update
2026/07/23 20:00 (dev-main)
License
Links
Downloads
14

Comments
comments powered by Disqus

Laravel Issue Reporter

Capture application errors and publish them as GitLab issues, with fingerprint-based deduplication.

pipeline status

What it does / what it doesn't

Does:

  • Captures exceptions and log messages through a dedicated issue-reporter log channel, or through the IssueReporter facade.
  • Fingerprints each error so recurring failures comment on the existing GitLab issue instead of creating a new one every time.
  • Scrubs sensitive data (secret-looking keys, query strings, credit-card-shaped numbers) before anything leaves the application.
  • Delivers through the queue by default, with a synchronous mode for local debugging.
  • Ships a issue-reporter:test command to verify the token, project and network path are correct.

Does not:

  • Replace an APM like Sentry or Bugsnag: there is no dashboard, no performance monitoring, no breadcrumbs, no session tracking. Errors become issues, nothing more.
  • Ship a working Bitbucket, GitHub or Jira driver. Bitbucket is declared but not implemented (see Bitbucket).
  • Guarantee zero-loss delivery under concurrency. The dedup logic uses a pessimistic lock on the fingerprint row to decide create-vs-comment, but a comment that arrives before its sibling create has finished can be dropped after a few retries (see Deduplication), and a create that exhausts its retries leaves that fingerprint without an issue until the row is fixed by hand. Losing a comment is an accepted trade-off; a duplicate issue is not.

Installation

composer require asdrubalp9/laravel-issue-reporter
php artisan vendor:publish --tag=issue-reporter-config
php artisan vendor:publish --tag=issue-reporter-migrations
php artisan migrate

Configuration

Add these to your .env:

ISSUE_REPORTER_GITLAB_TOKEN=glpat-xxxxxxxx
ISSUE_REPORTER_GITLAB_PROJECT=group/my-app

The token needs the api scope and at least the Reporter role on the project — it must be able to create issues and notes.

See Configuration reference for every other key.

Enabling capture

Add the issue-reporter channel to your log stack in config/logging.php:

'stack' => [
    'driver'   => 'stack',
    'channels' => ['single', 'issue-reporter'],
    'ignore_exceptions' => false,
],

'issue-reporter' => [
    'driver' => 'issue-reporter',
    'level'  => env('ISSUE_REPORTER_LOG_LEVEL', 'error'),
],

Anything logged at error level or above (through Laravel's default exception handler, or your own Log::error(...) calls) now flows through the reporter.

Verifying

php artisan issue-reporter:test

This creates a throwaway issue titled [test] IssueReporter smoke test and prints its URL. It bypasses deduplication entirely, so it always creates a new issue — run it as many times as you like, then close the issues it made. Pass --driver= to check a driver other than the configured default.

Manual usage

Beyond the log channel, the IssueReporter facade is available directly:

use Asdrubalp9\IssueReporter\Facades\IssueReporter;

// Report a caught exception, with extra context merged into the issue body.
IssueReporter::report($e, ['order_id' => 7]);

// Report a plain message instead of an exception.
IssueReporter::message('Disk almost full', [], 'critical');

// Force a specific fingerprint instead of the automatic one, so unrelated
// failures that should be tracked as "the same problem" collapse into one
// issue.
IssueReporter::report($e, ['fingerprint' => 'billing-failure']);

Deduplication

Every report is reduced to a fingerprint: by default it's a hash of the exception class, the normalized file path and line, and the message with paths, UUIDs, emails and numbers collapsed to placeholders (so Order 42 not found and Order 917 not found hash the same). Passing a fingerprint key in the context array (see above) overrides this and lets you group errors manually.

The first time a fingerprint is seen, an issue is created. Every subsequent occurrence increments a counter and — subject to the throttle below — posts a comment on the existing issue instead of opening a new one.

  • throttle_seconds (default 300): after a comment is posted for a fingerprint, further occurrences of the same fingerprint are silently counted but not re-reported until this many seconds have passed. This prevents a hot loop from flooding the issue with comments.
  • reopen_closed (default false): if the tracked issue was closed on GitLab, a new occurrence does not reopen it by default — it's just counted, on the assumption that a closed issue was closed on purpose. Set this to true to have a recurrence reopen the issue and comment on it.

Privacy

The Scrubber runs unconditionally on every report, before it is queued or sent anywhere — there's no way to disable it.

It redacts:

  • Any array key that looks sensitive (case-insensitive, and matching across snake_case, kebab-case and concatenated forms — so api_key, Api-Key and X-Api-Key all match), including a built-in base list of password, passwd, secret, token, api_key, apikey, authorization, auth, credit_card, card_number, cvv, ssn, private_key, session, cookie, access_token, refresh_token, remember_token and _token.
  • The same sensitive keys when they appear in the URL's query string.
  • Any run of digits that is 13–19 digits long (optionally grouped with spaces or hyphens, e.g. 4242 4242 4242 4242) and passes the Luhn checksum — regardless of the key it's under, since card numbers show up in free-text messages too.

The scrub_keys config value adds to the base list above; it cannot be used to shrink or disable it. There is no config option to turn scrubbing off.

Configuration reference

All keys live under config/issue-reporter.php.

Key Default Description
enabled true Master on/off switch for the whole package.
default 'gitlab' The tracker driver used when none is passed explicitly.
environments ['production', 'staging'] app.env values in which reporting is active; empty array means all.
minimum_level 'error' Minimum Monolog level that triggers a report.
drivers.gitlab.url 'https://gitlab.com' Base URL of the GitLab instance (self-hosted supported).
drivers.gitlab.token null Personal/project access token with api scope.
drivers.gitlab.project null Project path or numeric ID (group/project).
drivers.gitlab.timeout 10 HTTP timeout in seconds for GitLab API calls.
drivers.bitbucket.workspace null Reserved for the future Bitbucket driver; not read by any implemented code yet.
drivers.bitbucket.repository null Reserved for the future Bitbucket driver; not read by any implemented code yet.
drivers.bitbucket.username null Reserved for the future Bitbucket driver; not read by any implemented code yet.
drivers.bitbucket.password null Reserved for the future Bitbucket driver; not read by any implemented code yet.
drivers.null [] No configuration; the null driver discards everything.
queue.enabled true Whether delivery is dispatched to the queue (true) or run synchronously (false).
queue.connection null Queue connection used for the delivery job; null uses the app default.
queue.name 'default' Queue name used for the delivery job.
table 'issue_reporter_errors' Table name for the dedup/tracking model.
throttle_seconds 300 Minimum seconds between two comments on the same fingerprint.
reopen_closed false Whether a recurrence reopens a closed issue.
stack_frames 25 Number of stack frames included in the issue body.
labels ['bug', 'issue-reporter'] Labels applied to every created issue.
label_with_environment true Whether the current app.env is added as an extra label.
comment_includes_context true Whether recurrence comments include the report's context array.
ignore_exceptions list of framework exceptions (auth, validation, 404, 405, token mismatch) Exception classes (and subclasses) that never get reported.
scrub_keys [] Extra sensitive keys, added to the built-in base list (see Privacy).
emergency_log_channel 'single' Log channel used to record failures of the reporter itself.

Bitbucket

Bitbucket is declared in the config and registered in the TrackerManager so the extension point is explicit, but it is not implemented. Every method on BitbucketTracker throws UnsupportedDriverException — it fails loudly rather than silently dropping reports.

To use a different tracker today, register your own driver:

use Asdrubalp9\IssueReporter\TrackerManager;

app(TrackerManager::class)->extend('bitbucket', function ($app) {
    return new MyBitbucketTracker(/* ... */);
});

Your driver must implement Asdrubalp9\IssueReporter\Contracts\IssueTracker (createIssue, commentOnIssue, getIssueState, reopenIssue).

Testing

Run the package's own suite:

composer test

In the tests of a host application, swap the real tracker for Asdrubalp9\IssueReporter\Trackers\FakeTracker, which records every call instead of making HTTP requests:

use Asdrubalp9\IssueReporter\Trackers\FakeTracker;
use Asdrubalp9\IssueReporter\TrackerManager;

$tracker = new FakeTracker;
$this->app->make(TrackerManager::class)->extend('fake', fn () => $tracker);
config()->set('issue-reporter.default', 'fake');

// ... trigger the error ...

$this->assertCount(1, $tracker->created);

License

MIT. See LICENSE.