Skip to content

Issues

Issues

An issue is one error group — the same fingerprint the dashboard already groups occurrences by — plus the decision your team made about it.

It can also be a scheduled task that missed its slot, overran or keeps failing, or a scheduler that stopped. Those are conditions rather than occurrences and behave a little differently; see scheduled tasks. The kind column says which: exception, scheduler_stopped, scheduled_task_missed, scheduled_task_overrunning or scheduled_task_failing.

The row is an overlay, not a copy. Occurrences, stack traces and counts are read from the telemetry store every time. Delete the row and you lose the status, nothing else.

Status

open Nobody has decided anything yet
resolved Believed fixed. A later occurrence reopens it
ignored Known, deliberately not worth acting on. Never notifies
snoozed Quiet until snoozed_until, then open again
archived Quiet until it escalates — see below. Error groups only

A lapsed snooze is open again the moment it lapses — effectiveStatus() works it out on read, so nothing has to sweep the table.

What is news

A window contains hundreds of occurrences and nearly all of them are the same things that were there an hour ago. A monitor that announces all of them gets muted inside a week, so only two things are news:

  • New — a fingerprint this install has never recorded. This is the only fact the telemetry store genuinely cannot answer: it can tell you an exception happened, not that it never happened before.
  • Regression — a group marked resolved that has fired again since it was resolved. The occurrence must be newer than resolved_at, so re-reading an old window never counts as a regression. A regression reopens the issue automatically.

Everything else is recurring, recorded silently.

Announcements respect status: an ignored or snoozed issue is never announced, however hard it is firing.

Recording a pass

use Cbox\TelemetryInsights\Issues\IssueLedger;
use Cbox\TelemetryUi\Http\Api\RequestScope;

$changes = app(IssueLedger::class)->record(new RequestScope(period: '1h'));

foreach ($changes as $change) {
    if ($change->kind->isNews()) {
        // $change->issue, $change->kind, $change->count
    }
}

A backend that will not answer yields no changes rather than an exception: a monitoring pass must not be the thing that breaks.

Reading without remembering

record() writes: afterwards those fingerprints are known, so they are never new again. Only the scan calls it.

preview() gives the same answer without remembering it, and that is what the digest uses. A report that marked everything as seen would mean running the digest before the scan left the scan with nothing to announce — a read would quietly disarm the alerting.

Acting on one

php artisan telemetry-insights:issues                       # the working list
php artisan telemetry-insights:issue resolve aaaa0000 --release=v2.4.1
php artisan telemetry-insights:issue snooze  aaaa0000 --until="3 days"
php artisan telemetry-insights:issue ignore  aaaa0000
php artisan telemetry-insights:issue archive aaaa0000
php artisan telemetry-insights:issue assign  aaaa0000 --to=sylvester
php artisan telemetry-insights:issue reopen  aaaa0000 --by=sylvester --comment="Still happening"

A unique prefix of the fingerprint is enough; an ambiguous one is refused rather than guessed at.

Resolving is what makes regression detection mean anything — until a group has been called fixed, it can never come back. Record the release the fix shipped in and a later occurrence says which fix it escaped.

From your own code the same actions are one service:

use Cbox\TelemetryInsights\Issues\IssueActions;

app(IssueActions::class)->resolve($issue, by: $user->name, release: 'v2.4.1');

History

Every decision lands in the issue's history ($issue->events(), table telemetry_issue_events): what was done, who did it, through which door, and the comment that came with it. The doors are console, api, agent (the agent tools), scan (the scan opening an issue, recording a regression or resolving a condition that cleared) and code (your own calls). Pass an Attribution to say who:

use Cbox\TelemetryInsights\Issues\ActionSource;
use Cbox\TelemetryInsights\Issues\Attribution;

app(IssueActions::class)->ignore(
    $issue,
    new Attribution(ActionSource::Api, actor: $user->email, comment: 'Expected during deploys'),
);

Leave it out and the entry reads "via code". The history is append-only; nothing in the package edits it.

The dashboard pages carry the same decisions as a menu on the status cell, so you can resolve an issue where you read it. They are offers: the endpoint still checks manageTelemetryUi, so a viewer who can see an issue cannot close it. Build your own screen on the HTTP API when you want more.

Archive until escalating

Some error groups are known, low-grade and not worth anyone's morning — until they get worse. Ignoring one hides it for good; snoozing brings it back on a date that has nothing to do with it. Archiving keeps it quiet until its volume escalates past what it did before you archived it.

At archive time the group's hourly volume over the previous week (escalation.baseline_days) is read once from the logs store, and two limits are learned:

  • spike limit — the 95th-percentile hour × spike_factor (3). Over it for sustained_hours (2) running is a real, lasting rise.
  • burst limit — the worst hour × burst_factor (1.5). Over it in any single hour is something the group has never done.

The two exist because "more errors" comes in two shapes. A nightly job that throws sixty times at 02:00 has a low normal hour and a high peak; its next 02:00 is one hour, under the burst limit, and reopens nothing. Neither limit goes below min_volume (10). A store that does not answer at archive time gives the floor limits, and the history says so.

The check costs one bounded read, and only for an archived group the scan just saw fire. An escalation reopens the issue, writes "Escalated" to its history with the numbers, and is announced (announce.escalations) and fires escalation rules. Browser exceptions are counted from traces, not logs, so an archived frontend group stays archived until someone reopens it. Scheduled-task issues cannot be archived — they already clear and return on their own, precisely.

Stale issues

An open error group with no occurrence for stale.resolve_after_days (14; 7 and 30 are the other sensible values) is resolved by the scan, recorded as automatic in its history ("No occurrence for 14 days."). A later occurrence reopens it as a regression through the usual path. Ignored, snoozed and archived issues are left alone: they are a decision someone made. stale.enabled = false turns it off.

Spikes

A known issue firing materially harder than in the window immediately before it — not against a long-run average, because a rate that doubled in the last ten minutes is the thing worth interrupting someone for, while one creeping up all month belongs in the digest.

Two guards keep it honest: an issue must reach five occurrences before a multiple means anything (one becoming three is not a 3× spike), and a fingerprint with no baseline is a new issue, which is already its own announcement.

Detecting a spike costs one extra read, so it only happens when a rule is actually watching for one. See alerting.