LaravelPackages.net
Acme Inc.
Toggle sidebar
felicianopj/laravel-cashier-inspector

A local debugging and diagnostic dashboard for Laravel Cashier. Captures Stripe webhook processing, detects inconsistent billing states, and explains what went wrong.

8
0
v0.1.4
About felicianopj/laravel-cashier-inspector

felicianopj/laravel-cashier-inspector is a Laravel package for a local debugging and diagnostic dashboard for laravel cashier. captures stripe webhook processing, detects inconsistent billing states, and explains what went wrong.. It currently has 0 GitHub stars and 8 downloads on Packagist (latest version v0.1.4). Install it with composer require felicianopj/laravel-cashier-inspector. Discover more Laravel packages by felicianopj or browse all Laravel packages to compare alternatives.

Last updated

Laravel Cashier Inspector

The diagnostic toolkit for Laravel Cashier.

Laravel Cashier Inspector is a local debugging and diagnostic dashboard for Laravel Cashier. It captures Stripe webhook processing, detects inconsistent billing states, and explains what went wrong.

The Cashier Inspector dashboard listing webhook deliveries with severity badges, status, event type, customer and duration

Cashier answers Stripe with HTTP 200 even when no billable model matches the event's customer. Stripe records a successful delivery, the application does nothing, and nothing anywhere raises an error. That is the class of failure this package exists to make visible:

A warning row in the delivery list opening into its event page: processing status Handled in 40 ms, a warning that no local billable model matches the Stripe customer, and the suggested checks for it

Early release. Feature complete for what it sets out to do, but not yet used widely against real Stripe traffic, so the API may still change in a minor release while the version stays below 1.0.0.

Requirements

  • PHP ^8.2
  • Laravel ^11.0 | ^12.0 | ^13.0
  • Laravel Cashier Stripe ^15.0 | ^16.0

Cashier is a hard requirement rather than something you are expected to have already, so Composer will refuse to install this package alongside a Cashier version it does not support instead of failing later at runtime.

Every supported Laravel version is covered by the test matrix, against both lowest and stable dependency resolutions: Laravel 11 and 12 on PHP 8.2, 8.3, 8.4 and 8.5, and Laravel 13 on PHP 8.3, 8.4 and 8.5. Laravel 13 is not tested on PHP 8.2 because it requires a Pest Laravel plugin major that itself requires PHP 8.3; the package still supports PHP 8.2 on Laravel 11 and 12. Both Cashier majors are covered by the same runs: the lowest resolutions land on Cashier 15 and the stable ones on Cashier 16.

On PHP 8.5, the oldest resolution reports a deprecation for every test. It comes from PDO::MYSQL_ATTR_SSL_CA in Laravel 11's schema state and in Testbench's own skeleton config, not from this package, and the suite passes. The tests run on SQLite and never touch that constant.

Installation

composer require felicianopj/laravel-cashier-inspector:^0.1 --dev
php artisan cashier-inspector:install
php artisan migrate

The install command publishes the package config and migrations, checks that Laravel Cashier Stripe and its own database schema are present, warns if STRIPE_WEBHOOK_SECRET isn't set, and prints the dashboard URL.

No published views or frontend assets are required for normal installation. Laravel's package discovery registers the service provider automatically.

To publish config or migrations individually:

php artisan vendor:publish --tag=cashier-inspector-config
php artisan vendor:publish --tag=cashier-inspector-migrations

Dashboard

The dashboard lives at /cashier-inspector by default, configurable via CASHIER_INSPECTOR_PATH. It's enabled by default only in local environments (CASHIER_INSPECTOR_ENABLED).

By default it shows problems only - errors, warnings, unmatched events, and processing that's taking too long - with filters and search to see everything else. New deliveries are announced as they arrive:

The dashboard announcing three new events in a banner, which are loaded into the list when clicked

Search matches Stripe event, customer, subscription, invoice, and checkout session ids, the local billable model id, and the local billable email. Email searching works even though emails are redacted out of stored payloads: the term is matched against your own customer table, so no copy of the address has to be kept here. Only the model configured through Cashier (Cashier::useCustomerModel()) is searched, on its email column.

Processing timeline

Every event page carries a timeline of the phases of the request that delivered it, with how long each one took:

A five-phase processing timeline: request received, event captured, Cashier handler, diagnostics, response

These five are the phases that can honestly be observed from outside Cashier: everything its handler does is a single window, and any listeners the application registers fall inside that window too. When Cashier has no handler for the event type, the handler phase is recorded as skipped; when it throws, as failed, carrying the exception.

Turn it off with CASHIER_INSPECTOR_RECORD_STEPS=false on an installation with heavy webhook traffic that never reads a timeline. Deliveries recorded while it is off, or before this feature existed, still show their received and resolved times.

Every event page also has a "Copy diagnostic report" button that produces a sanitized, plain-text summary suitable for pasting into a GitHub issue, Discord, support, or an LLM.

Instrumenting Cashier's route (optional)

Failures are captured without any of this: an exception reporting hook records the class, message and trace, and a terminating middleware records that the request ended abnormally even when the hook never runs.

That pair has one gap. An exception the application chooses not to report - anything in dontReport, or a handler that reports selectively - leaves only the fallback, so the event page says the request ended with a 500 and cannot say what threw. Turn on:

CASHIER_INSPECTOR_ROUTE_MIDDLEWARE=true

and this package attaches middleware to Cashier's own webhook route. Every Throwable it lets escape is recorded with its class, message and trace, then rethrown unchanged, so Cashier and the application's error handling behave exactly as before. The route is found by the controller it resolves to, so moving Cashier with CASHIER_PATH needs no configuration here.

It does not make signature verification separately measurable - Cashier applies that from its controller's constructor, so it runs inside the route dispatch - and it does not report which local subscription Cashier resolved, which the handlers do inline with no seam to observe.

Telescope

When Laravel Telescope is installed, every entry it records while a Stripe webhook is being processed - the request, the queries, the events, the exceptions - is tagged with that event's id, and each event page carries a "View in Telescope" link filtered to those entries. So a diagnosis here leads straight to the queries that ran while it happened.

Telescope is not a dependency and nothing here runs without it. The link is hidden when Telescope is absent or disabled, and CASHIER_INSPECTOR_TELESCOPE_LINKS=false suppresses it while leaving Telescope alone.

Nightwatch is not integrated: it exposes no supported way to attach an identifier to the current request, and no per-request URL to link to.

Security and privacy

The dashboard is never public by default. In production it stays disabled until explicitly enabled, and an authorization callback is required:

use FelicianoPJ\CashierInspector\CashierInspector;

CashierInspector::auth(function (Illuminate\Http\Request $request): bool {
    return $request->user()?->can('viewCashierInspector') ?? false;
});

Without a callback, access is restricted to the local environment.

Webhook payloads can contain personally identifiable and commercially sensitive data. By default, these paths are redacted before a payload is ever stored:

  • data.object.customer_email
  • data.object.customer_details
  • data.object.metadata
  • data.object.email
  • data.object.name
  • data.object.phone
  • data.object.address
  • data.object.shipping
  • data.object.receipt_email
  • data.object.billing_details

email, name, phone, address, shipping and metadata are masked under data.previous_attributes too, which carries the old values of whatever changed. The defaults err towards masking: name is a product name on product.* events, and is redacted there as well.

Redaction paths are configurable, support dot-notation with wildcards, and can be disabled entirely with CASHIER_INSPECTOR_REDACTION_ENABLED, which isn't recommended outside local development.

Raw payload storage is environment-conditional (CASHIER_INSPECTOR_STORE_PAYLOADS): on by default in local environments, off by default everywhere else. Exception stack traces are never stored by default (CASHIER_INSPECTOR_STORE_EXCEPTION_TRACES).

Cashier Inspector never modifies Laravel Cashier's own tables - it only reads from them for diagnostics, and writes to its own cashier_inspector_* tables.

Diagnostic rules

Every captured event is run through a set of rules. A rule that triggers records a finding against the event, which then shows on the event page, in the copied diagnostic report, and in cashier-inspector:check. Warning and error findings also make the event count as a problem, so it stays in the dashboard's default view.

These rules ship with the package:

| Rule | Code | Severity | Triggers when | | --- | --- | --- | --- | | ProcessingExceptionRule | processing_exception | error | Cashier threw while handling the webhook | | IncompatibleCashierSchemaRule | cashier_schema_incompatible | error | Cashier's own tables or columns are missing | | MissingWebhookSecretRule | webhook_secret_missing | warning | STRIPE_WEBHOOK_SECRET is not configured | | DuplicateDeliveryRule | duplicate_delivery | warning | The same Stripe event was delivered more than once | | TestLiveModeMismatchRule | mode_mismatch | warning | The event's mode does not match the configured Stripe key | | MissingLocalSubscriptionRule | missing_local_subscription | warning | The event names a subscription Cashier has no row for | | MissingBillableModelRule | missing_billable_model | warning | The event's customer resolves to no local billable model | | DuplicateSubscriptionTypeRule | duplicate_subscription_type | warning | One billable model has two valid subscriptions of the same type | | SlowProcessingRule | slow_processing | warning | Processing took longer than the configured threshold | | SubscriptionStatusMismatchRule | subscription_status_mismatch | warning | The local subscription status differs from Stripe's | | SubscriptionPriceMismatchRule | subscription_price_mismatch | warning | The local subscription prices differ from Stripe's | | UnhandledWebhookRule | webhook_unmatched | info | Cashier has no handler for this event type |

The two mismatch rules compare local Cashier state against a live fetch from Stripe. They are opt-in and off by default (CASHIER_INSPECTOR_STRIPE_API_CHECKS), since enabling them makes a live Stripe API call synchronously, during webhook processing.

Rules are a list in the published config: remove one and it stops running, add your own and it runs alongside the rest. See EXTENDING.md for the contract and a worked example.

Health check

php artisan cashier-inspector:check

Reports whether the pieces this package depends on are in place: Cashier and its schema, Cashier Inspector's own tables, the Stripe secret and webhook secret, Cashier's billable customer model, whether any webhook events have arrived recently, and what the diagnostic rules have already found on the events still stored.

INFO   Cashier Inspector's own tables exist.
INFO   STRIPE_SECRET is configured.
INFO   STRIPE_WEBHOOK_SECRET is configured.
INFO   Billable model was detected [App\Models\User].
INFO   4 webhook events were received in the last 24 hours.
ERROR  Diagnosed on the events that are still stored: 1 processing_exception.

It exits non-zero only when something is genuinely broken, so it can gate a deploy. A missing webhook secret is reported as a warning and still exits zero

  • that is a normal state in local development, and a check that failed on it would just get switched off.

The recent-events window defaults to 24 hours (CASHIER_INSPECTOR_RECENT_EVENTS_WINDOW_HOURS).

Inspecting one event

php artisan cashier-inspector:event evt_123

Prints what was captured and diagnosed for a single Stripe event, followed by every delivery attempt with its status, severity, and duration. The summary is the same text the dashboard's "Copy diagnostic report" button produces, and neither includes the raw payload.

Useful where the dashboard is not reachable - an SSH session on a production box, or a CI job. Exits non-zero when no event was captured for that id.

Filling in missing associations

Searching by customer id finds every event recorded against that customer, so an event stored without one is invisible to that search. Some Stripe objects reference no customer at all - an invoice_payment names only its invoice - and an event captured before a billable model had its stripe_id set matched nothing at the time.

php artisan cashier-inspector:backfill-customers

This fills both, and only ever fills blanks. Run it after setting a stripe_id on an existing customer, or after a burst of webhooks arrived out of order. It also re-reads stored payloads to recover correlation ids from events captured by a version of this package that was not yet reading them, which is what an existing installation needs after upgrading.

An event whose payload names no customer, invoice or subscription - a charge created outside any customer - has nothing to correlate through, and its customer column stays empty.

Retention

Old events, deliveries, and diagnostics can be pruned:

php artisan cashier-inspector:prune

This isn't scheduled automatically. Add it to the application's own scheduler to run it regularly:

$schedule->command('cashier-inspector:prune')->daily();

The retention period defaults to 7 days (CASHIER_INSPECTOR_RETENTION_DAYS) and can be overridden per run with --days.

Troubleshooting

TROUBLESHOOTING.md covers the recurring Cashier and Stripe webhook failures: what each looks like in the dashboard, what causes it, and how to fix it. Each section names the finding this package records, so a diagnosis on an event page leads straight to the explanation.

Changelog

See CHANGELOG.md for what shipped in each release.

Disclaimer

Laravel Cashier Inspector is an independent open-source project and is not affiliated with or endorsed by Laravel LLC or Stripe, Inc.

License

MIT

Comments