Troubleshooting
Work from the symptom below, then confirm that cached configuration and long-running processes contain the same code and settings as the web process.
Contents
Section titled “Contents”- A finding is not discovered
- A finding key cannot be resolved
- Duplicate findings appear
- Context creates unexpected identities
- Scheduled detection does not run
- A scheduled detection overlaps or remains locked
- Asynchronous detection runs synchronously
- A queued detection is not processed
- Verification or resolution is unavailable
- Verification or resolution fails
- Deleted models keep their findings
- Restored models have no findings
- A cursor appears stuck or skips records
- Filament actions are missing
- Affected-model links are missing
- Assignment is missing or incomplete
- Configuration changes have no effect
A finding is not discovered
Section titled “A finding is not discovered”Check the discovery contract in this order:
- The configured directory exists relative to the Laravel application’s base path.
- The configured root namespace maps the directory to the correct autoloadable prefix; a trailing backslash is optional.
- The class namespace and filename mirror their path below that directory.
- The class is concrete and extends
DataHealth\Finding. - Composer can autoload the application namespace.
- The process has been restarted after deployment when it is long-running.
For the default mapping, app/DataHealth/PaidOrderMarkedPending.php must declare App\DataHealth\PaidOrderMarkedPending.
If configuration is cached, inspect the active value rather than only the source file:
php artisan tinkerconfig('data-health.directories');See Finding Discovery for custom paths and namespaces.
A finding key cannot be resolved
Section titled “A finding key cannot be resolved”Data Health reconstructs a finding record by matching its stored key to a currently discovered class. Resolution fails when the class is no longer discovered, its basename changed, or its Key attribute changed.
Restore the old key temporarily or migrate existing rows to the new key:
DB::table('data_health_findings') ->where('key', 'OldFindingKey') ->update(['key' => 'new-stable-key']);Ensure the destination key belongs to exactly one discovered class before migrating. Prefer an explicit, permanent Key attribute for findings that may be renamed.
Duplicate findings appear
Section titled “Duplicate findings appear”The database considers key, model_type, model_id, and context_hash together. Rows that look similar in an interface may still have different context, keys, or morph types.
Compare those four columns first. Common causes include:
- context containing a timestamp, generated UUID, or other changing value;
- list values returned in a different order;
- a renamed finding without a stable
Key; - a changed Laravel morph map; or
- concurrent writes without the published unique identity index.
Confirm that data_health_findings_identity_unique exists. Do not remove the unique index: application-level firstOrCreate() alone cannot prevent every race between workers.
Context creates unexpected identities
Section titled “Context creates unexpected identities”Associative keys are recursively sorted before hashing, so their insertion order does not matter. List order remains significant, and every value contributes to the hash.
Keep identity context deterministic. Sort lists when their order has no meaning, normalize dates and numeric values, and leave volatile display-only values out of context. Changing the structure of buildContext() affects future identity matching but does not rewrite stored hashes.
See Context for stable and unstable examples.
Scheduled detection does not run
Section titled “Scheduled detection does not run”Run through the complete execution chain:
- The finding is discovered.
- It implements
DataHealth\Contracts\CanDetect. - It has a valid
Scheduledfive-field cron expression. data-health.scheduler.enabledistruein the running process.php artisan schedule:listshows the event and the expected next run time.- Laravel’s system cron or
php artisan schedule:workis running. - The application timezone matches the intended schedule.
- A previous execution is not holding the overlap lock.
- For
Async, an appropriate queue worker is running.
Data Health registers schedules only in console processes. Changing code or configuration does not update an already-running scheduler or worker; restart it after deployment.
A scheduled detection overlaps or remains locked
Section titled “A scheduled detection overlaps or remains locked”Each scheduled finding uses Laravel’s overlap prevention and one-server execution. A still-running or stale lock can prevent the next occurrence.
First confirm whether an earlier process is genuinely running. If it is not, clear Laravel’s scheduler locks:
php artisan schedule:clear-cacheIn a multi-server deployment, all scheduler hosts need the same lock-capable cache store. An array, local file, or otherwise isolated store cannot coordinate independent servers. Avoid clearing locks while a valid detection run is still active because doing so may allow overlapping work.
Asynchronous detection runs synchronously
Section titled “Asynchronous detection runs synchronously”The Async attribute dispatches a job, but Laravel’s sync queue connection executes dispatched jobs immediately. Select a background connection globally or on the attribute:
#[Async(queue: 'data-health', connection: 'redis')]Then run a worker for the selected destination. Also verify that the finding has both Scheduled and Async; Async affects scheduled detection and does not transform arbitrary direct calls to detect() into queued work.
A queued detection is not processed
Section titled “A queued detection is not processed”Confirm all of the following:
- a worker is running on the attribute’s connection and queue;
- the queue name matches exactly;
- failed jobs have been inspected;
- the worker was restarted after code changes;
- the application can deserialize and autoload the finding class; and
- the cache store used for unique jobs is available.
For example:
php artisan queue:work redis --queue=data-healthphp artisan queue:failedDetection jobs are unique by finding class. A queued or running job for the same finding may intentionally prevent another from being dispatched. If jobs remain unique unexpectedly, inspect the shared cache and failed worker state rather than repeatedly dispatching duplicates.
Verification or resolution is unavailable
Section titled “Verification or resolution is unavailable”Verification is available only when the finding implements CanVerify; resolution requires CanResolve. The method must have the corresponding public signature:
public function verify(): bool|callable|string;public function resolve(): bool|callable|string;The stored key must also resolve to that finding class, and the affected model must still be loadable so Data Health can reconstruct the finding. Add Description to the method to provide a useful action label, but note that a description alone does not enable the capability.
Verification or resolution fails
Section titled “Verification or resolution fails”The final result must be a boolean. A callable is invoked through Laravel’s container, and a returned class name must implement Verifier or Resolver respectively. Typical failures are:
- returning an unsupported string or value;
- naming a class that does not implement the required contract;
- an unresolvable dependency in the callable or handler constructor;
- relying on missing or stale context; or
- trying to use an affected model that has been deleted.
A false result is not an exception and leaves status unchanged. Exceptions propagate to the caller, and Data Health does not wrap the fix in a database transaction. Make fixes idempotent and add an application transaction when several writes must succeed together.
Deleted models keep their findings
Section titled “Deleted models keep their findings”This is the default retention behavior. To remove finding records when their affected model is deleted, set:
DATA_HEALTH_AUTO_DELETE_ENABLED=trueRebuild cached configuration and make sure the deletion happens through Eloquent model events. Bulk query deletes do not dispatch per-model deleted events, and direct database deletes bypass Eloquent completely.
Restored models have no findings
Section titled “Restored models have no findings”With automatic cleanup enabled, deleting a soft-deletable model removes its finding records. Restoring that model does not recreate them. Run the relevant detector again after restoration if current problems should be represented.
If findings must survive soft deletion as audit records, leave automatic cleanup disabled or implement an application-specific lifecycle policy.
A cursor appears stuck or skips records
Section titled “A cursor appears stuck or skips records”CheckCursor scans ascending numeric id values and resets after reaching the maximum ID present in the supplied query. Check that:
- the query targets a model with a numeric
idcolumn; - the limit is positive;
- repeated scans use the same key when they should share progress;
- unrelated scans use different keys;
- the query’s filters remain stable throughout a pass; and
- the cursor row in
data_health_cursorscontains the expectedlast_id.
An empty collection at the end of a pass is valid and resets the cursor to 0; the following call starts again. Changing query filters between batches can cause records to enter or leave the remaining ID range, so use a stable scope where complete coverage matters.
When each selected model must be checked against a third-party API, use a small cursor batch to limit the number of remote calls in each run. If the API itself is the paginated data source and there is no local Eloquent candidate query, persist the API’s page or continuation token in application storage instead. See Large Datasets for the local-model pattern.
Filament actions are missing
Section titled “Filament actions are missing”Use the action’s corresponding capability as the first check:
| Missing capability | Requirement |
|---|---|
| Detect on a finding type | The class implements CanDetect. |
| Verify a record | The class implements CanVerify. |
| Resolve a record | The class implements CanResolve. |
| Assign a record | data-health-filament.assignee_types provides at least one valid type. |
Confirm that DataHealthPlugin::make() is registered on the current panel. For standalone components, confirm that the component is rendered inside a working Filament and Livewire installation.
The package does not provide application-specific authorization. If actions must be restricted, apply the panel, resource, middleware, and standalone-component authorization described in Authorization.
Affected-model links are missing
Section titled “Affected-model links are missing”A model label remains unlinked when no usable destination can be produced. Check the resolution chain:
- the affected model relationship can still be loaded;
- a configured
model_view_routesentry matches its class or stored morph type and names an existing route; - the custom URL resolver returns a non-empty URL; or
- inside a panel, the model has a registered Filament resource with a permitted
viewpage.
The panel-resource fallback is unavailable to standalone components outside a panel. Configure a named route or custom resolver for those contexts. See Configuration and Model Links for examples.
Assignment is missing or incomplete
Section titled “Assignment is missing or incomplete”assignee_types must contain the class name of an invokable provider. The provider must return an array of Filament MorphToSelect\Type instances, each with a usable title attribute or label configuration.
If the assign action appears but expected records do not, inspect query restrictions defined on the type and the model’s database connection. If an existing assignment displays poorly, confirm the stored assignee_type still matches the model’s current morph class and that the assigned row still exists.
Configuration changes have no effect
Section titled “Configuration changes have no effect”Clear or rebuild cached configuration and restart long-running processes:
php artisan config:clearphp artisan config:cachephp artisan queue:restartRun only the cache command appropriate to the deployment workflow; config:cache replaces a cleared cache immediately. Also restart schedule:work, process supervisors, and persistent PHP application servers when they retain the old container.
Finally, verify that the setting was changed in the correct file: core behavior uses config/data-health.php, while Filament presentation uses config/data-health-filament.php.