laravel-issue-reporter maintained by asdrubalp9
Laravel Issue Reporter
Capture application errors and publish them as GitLab issues, with fingerprint-based deduplication.
What it does / what it doesn't
Does:
- Captures exceptions and log messages through a dedicated
issue-reporterlog channel, or through theIssueReporterfacade. - 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:testcommand 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(default300): 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(defaultfalse): 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 totrueto 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-caseand concatenated forms — soapi_key,Api-KeyandX-Api-Keyall match), including a built-in base list ofpassword,passwd,secret,token,api_key,apikey,authorization,auth,credit_card,card_number,cvv,ssn,private_key,session,cookie,access_token,refresh_token,remember_tokenand_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.