›
›
›
  1. docs
  2. ›
  3. byrcsc/laravel-customer-health
1.x
Browse documentationOpenClose

Getting started

  • Introduction
  • Installation and setup
  • Quick start

Core concepts

  • Product events
  • Features and milestones
  • Onboarding
  • Health scores
  • Summaries and tenancy

Operations

  • Querying customer health
  • Queueing events
  • Recomputing scores
  • Retention and erasure
  • Production operations

Reference

  • Configuration
  • Public API
  • Events
  • Console commands
  • Database storage
  • Testing
  • Troubleshooting

Getting started

  • Introduction
  • Installation and setup
  • Quick start

Core concepts

  • Product events
  • Features and milestones
  • Onboarding
  • Health scores
  • Summaries and tenancy

Operations

  • Querying customer health
  • Queueing events
  • Recomputing scores
  • Retention and erasure
  • Production operations

Reference

  • Configuration
  • Public API
  • Events
  • Console commands
  • Database storage
  • Testing
  • Troubleshooting

byrcsc/laravel-customer-health · 1.x

Health scores.

Health score declarations combine weighted signals into explainable values, states, and history.

Use a health score when customer state should be calculated from several observable signals. Each computation stores the inputs and contributions that produced its value.

Score declarations

A score extends HealthScore and declares signals plus state thresholds:

namespace App\CustomerHealth;

use ByRcsc\LaravelCustomerHealth\Scoring\HealthScore;
use ByRcsc\LaravelCustomerHealth\Scoring\Signals\FeatureActivity;
use ByRcsc\LaravelCustomerHealth\Scoring\Signals\FeatureAdopted;
use ByRcsc\LaravelCustomerHealth\Scoring\Signals\RecentActivity;

final class CustomerHealthScore extends HealthScore
{
    public static string $name = 'customer_health';

    public function signals(): array
    {
        return [
            new RecentActivity(days: 30, weight: 2),
            new FeatureAdopted(feature: 'workflows', weight: 1),
            new FeatureActivity(feature: 'reports', days: 30, weight: 1),
        ];
    }

    public function states(): array
    {
        return [
            'at_risk' => 0,
            'needs_attention' => 50,
            'healthy' => 75,
        ];
    }
}

The default name is the snake-cased class basename. Names must be non-empty and unique among registered scores.

Weight normalization

Every signal returns an integer from 0 through 100 and a finite positive weight. The package normalizes weights before multiplying each raw value.

Weights of 2, 1, 1 therefore become 0.5, 0.25, 0.25. Scaling every weight by the same factor does not change the result.

The final sum is rounded to an integer. A signal value outside 0 through 100 throws InvalidSignalValueException and stores no score.

Built-in signals

SignalResult
RecentActivity($days, $weight)100 when any event falls inside the inclusive UTC window
FeatureAdopted($feature, $weight)100 when the feature has a permanent milestone
FeatureActivity($feature, $days, $weight)100 when the feature has activity inside the window
DistinctActors($days, $weight)100 when any identified actor has activity inside the window
OnboardingProgress($checklist, $weight)The checklist percentage from 0 through 100

The built-in activity signals are binary. Write a custom signal when a count, ratio, contract value, or another application rule should produce intermediate values.

Custom signals

Implement Signal:

namespace App\CustomerHealth\Signals;

use ByRcsc\LaravelCustomerHealth\Contracts\Trackable;
use ByRcsc\LaravelCustomerHealth\Scoring\Signal;

final readonly class SeatUtilization implements Signal
{
    public function __construct(private float $signalWeight) {}

    public function evaluate(Trackable $subject): int
    {
        $used = $subject->users()->count();
        $available = max(1, $subject->seat_limit);

        return min(100, (int) round(($used / $available) * 100));
    }

    public function weight(): float
    {
        return $this->signalWeight;
    }
}

Implement WindowedSignal as well when the signal reads a declared day window. Its windowDays() method lets the recompute command warn when raw-event retention is shorter than a scoring window.

State selection

State thresholds are inclusive lower bounds. The package sorts thresholds by value, then selects the highest threshold less than or equal to the score.

Every score must declare one state starting at 0. State names must be non-empty, thresholds must be unique integers from 0 through 100, and the states array cannot be empty.

Compute and read history

$result = CustomerHealth::compute($team, 'customer_health');

$result->score;
$result->value;
$result->state;
$result->breakdown;
$result->computedAt;

$latest = CustomerHealth::score($team, 'customer_health');
$history = CustomerHealth::scoreHistory($team, 'customer_health');

Omit the score argument to use the first registered score. score() returns null before the first computation. scoreHistory() returns stored results in ascending computation order.

Each computation dispatches HealthScoreComputed after its transaction commits. HealthStateChanged fires when the previous and current states differ, including the first computation where $from is null.

What to read next

  • Summaries and tenancy to query current state without scanning score history.
  • Recomputing scores to update scores in batches.
  • Public API for exact signal and score method signatures.
PreviousOnboardingNextSummaries and tenancy
View source

On this page

  1. Score declarations
  2. Weight normalization
  3. Built-in signals
  4. Custom signals
  5. State selection
  6. Compute and read history
  7. What to read next