Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Cairn Laravel Package

divoto/cairn

Cairn is a lightweight Laravel package from Divoto that helps you mark a clear path through your codebase with a clean, expressive API. It integrates smoothly with Eloquent, ships with sensible defaults, and makes complex features easier to build and maintain in any Laravel app.

View on GitHub
Deep Wiki
Context7

Getting Started

Cairn is a self-hosted, rollup-backed analytics package. Install it, publish its config, run the package migrations, and the pageview middleware on the web group starts recording immediately — you do not wire up tracking calls by hand.

  1. Publish the config and read config/cairn.php first. It is the whole control surface: recorders, the widgets list, the privacy gate, and the API's enabled state and middleware.
  2. Open the dashboard. As of v1.3.0 it opens on Today, charted hour by hour. Add ?range=30d to the URL if you want the old default while you get your bearings.
  3. Add the two new panels if you curate your widget list. If your published config enumerates widgets, Divoto\Cairn\Widgets\Shipped\SlowestRoutes::class and Divoto\Cairn\Widgets\Shipped\BrokenPages::class do not appear until you add them, then php artisan config:cache.

A good first use case is triage, not traffic counting: open Broken pages, sort by status, and read the referrer column. A 404 with a referrer is someone else's dead link pointing at you (fix with a redirect); a 5xx row names the page with the bug. Pair it with Slowest routes next to Core Web Vitals so "the server is slow" and "the page is slow" are two separate answers rather than one vague complaint.

Implementation Patterns

Ranking reports through the JSON API. Grouped reports are now ranked by the first metric in metrics, highest first, before limit is applied — so group_by=language&metrics=pageviews&limit=5 genuinely returns the five busiest languages. Order your metrics list deliberately; the first one is the sort key.

// Dashboard links and API calls now share a vocabulary
$response = Http::withToken($token)->get('/cairn/api/report', [
    'range'      => '30d',
    'group_by'   => 'route',
    'metrics'    => ['errors', 'pageviews'],
    'route'      => '/checkout',   // dimension filter, honoured since 1.3.0
    'limit'      => 20,
]);

Dimension filters mirror the dashboard. route, country, channel, language, screen_class and entry_url narrow the API report the same way they narrow the dashboard, so a query string copied from a dashboard link works unchanged. Combining two filters, or a filter with a conflicting group_by, is a 422 — it was never rolled up.

Error reporting with Metric::Errors. Errors are measured for every entry dimension, so "errors on this route" is a real number and not an approximation from status codes. Use Dimension::Status for a coarse 4xx/5xx split and Dimension::ErrorPage when you need status + URL + referrer together; its JSON key is decoded for you into {"status": 404, "url": "/old-pricing", "referrer": "blog.example.com"}.

Floors on noisy tables. Report::minimum() drops rows below a metric floor before ordering and the limit, so a ranked table still fills to its limit from qualifying rows rather than returning a short list. This is the right tool for "top routes with at least N pageviews" in your own widgets and exports.

Handling not_found entries. These are a distinct entry type, not pageviews: they start no session, count toward no visitor figure, and leave the route empty. That means a scanner walking a path list moves no number except Broken pages — which is exactly what you want from a security-questionnaire perspective. They are recorded by a global middleware that only catches a GET answered 404 that is not asking for JSON. Add known-noisy probes to Cairn::ignore() alongside your existing exclusions.

Backfill after upgrading. Routed pages answered 4xx/5xx have carried their status since 1.0 but were never rolled up into the new panels. Re-run the rollup over retained raw entries to give Broken pages history:

php artisan cairn:rollup --from=2026-09-24 --to=2026-10-24 --period=all

Nothing new is recorded for Slowest routes — it reads figures the rollup has always stored. Unrouted 404s were never recorded at all, so those fill from now on regardless of backfill.

Gotchas and Tips

The dashboard default changed. Today instead of the last 30 days. Anything that relied on "open the dashboard and see a month" — screenshots in docs, onboarding tours, an operations runbook — now shows one day. Ranges are still one click away, and any URL or bookmark naming a range keeps opening on it. Pin ?range= in saved links rather than relying on the default.

Unknown API parameters are now a 422, not ignored. This is the upgrade's most likely breaking change for existing clients. A cache-busting _=1730… appended by a fetch wrapper, a legacy period=7d, group_by=bogus or an unrecognised metric all now fail loudly with source.parameter naming the offender and listing accepted values. Audit what your client actually sends before deploying. limit above 1,000 is the one exception: it is capped rather than refused.

A disabled API is a 404 before middleware runs. The availability check now precedes configured middleware, so an endpoint behind auth:sanctum in an app without Sanctum returns 404 rather than 500, and with Sanctum returns 404 rather than 401. That is deliberate — it stops anonymous callers learning something is there. Don't debug a "missing" endpoint as an auth problem.

not_found recording is opt-out, not opt-in. A published config without a not_found key has it on. Set recorders.PageViews::class.not_found to false to record none. Stale published configs therefore start recording after the upgrade without any action — which is usually fine, but is a surprise if you audit what your config declares.

Unrouted URLs are scrubbed hard and lose their query string. Numeric ids, UUIDs, ULIDs and any token-shaped segment become {id}, and the entire query string — UTM parameters included — is dropped. You cannot attribute broken inbound campaign links from the URL; use the referrer on the Broken pages row instead. This is intentional: a mistyped reset link is still a credential.

The privacy gate applies to not_found exactly as to pageviews — Do Not Track, Global Privacy Control, opt-out, consent, bots and sampling. So an opted-out crawler or a DNT browser produces no broken-page rows, and a suspiciously quiet Broken pages panel may be a consent-configuration issue rather than a healthy site.

Dimension::ErrorPage keeps only the hundred most frequent combinations per bucket. Failed URLs come from whatever a stranger types, so the long tail is capped by design. If you need a specific old URL, query raw entries rather than the rollup.

Slowest routes silently excludes low-traffic routes. Fewer than five pageviews in the period and the route is left out, because an average of one request is noise. On a small site the panel looks broken when it is behaving correctly; widen the range before assuming the route is missing.

After adding widgets, re-run config:cache. The panels are invisible until the class strings are in the published list and the cached config reflects it — a stale cache is the usual reason a correctly-edited config changes nothing.

Extension points. Custom widgets (rather than editing shipped ones) and the recorders config keys are the intended seams. Reaching for the rollup tables directly couples you to internals that the 1.3.0 changes to key encoding and ranking behaviour would have broken.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource