Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 65 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@ Track page views, API calls, user signups, or any other countable events.
- [Using the Redis Driver](#using-the-redis-driver)
- [Usage](#usage)
- [Recording Metrics](#recording-metrics)
- [Recording with Values](#recording-with-values)
- [Recording with Categories](#recording-with-categories)
- [Recording with Dates](#recording-with-dates)
- [Recording Hourly Metrics](#recording-hourly-metrics)
- [Recording for Models](#recording-for-models)
- [Recording with Custom Attributes](#recording-with-custom-attributes)
- [Capturing & Committing](#capturing--committing)
Expand Down Expand Up @@ -148,7 +150,7 @@ Which ever method you use, metrics are recorded in the same way. Use whichever y

For the rest of the documentation, we will use the `metric` helper for consistency and brevity.

### Metric Values
### Recording with Values

By default, metrics have a value of `1`. You may specify a custom value in the `record` method:

Expand Down Expand Up @@ -207,6 +209,61 @@ metric('jobs:completed')
->record(1250);
```

### Recording Hourly Metrics

By default, metrics are recorded at the **daily** level. For metrics that require hour-level granularity, you may use the `hourly()` method:

```php
// Track API requests by hour
metric('api:requests')
->hourly()
->record();
```

Hourly metrics include the hour (0-23) in addition to the year, month, and day, allowing you to track metrics at a more granular level:

```php
use Carbon\Carbon;

// Record API requests for a specific hour
metric('api:requests')
->date(Carbon::parse('2025-10-19 14:30:00'))
->hourly()
->record();

// This will be stored with hour = 14
```

Hourly metrics are stored separately from daily metrics, even for the same metric name:

```php
metric('page:views')->record(); // Daily metric (hour = null)
metric('page:views')->hourly()->record(); // Hourly metric (hour = current hour)
```

You can query hourly metrics using the `thisHour()`, `lastHour()`, and `onDateTime()` methods:

```php
use DirectoryTree\Metrics\Metric;

// Get metrics for this hour
$metrics = Metric::thisHour()->get();

// Get metrics for last hour
$metrics = Metric::lastHour()->get();

// Get metrics for a specific date and hour
$metrics = Metric::onDateTime(Carbon::parse('2025-10-19 14:00:00'))->get();

// Get API requests for the current hour
$requests = Metric::thisHour()
->where('name', 'api:requests')
->sum('value');
```

> [!tip]
> Use hourly metrics sparingly, as they create 24x more database rows than daily metrics. Reserve hourly tracking for metrics that genuinely benefit from hour-level granularity.

### Recording for Models

Associate metrics with Eloquent models using the `HasMetrics` trait:
Expand Down Expand Up @@ -300,16 +357,16 @@ metric('api:requests')
Custom attributes are included in the metric's uniqueness check, meaning metrics with different attribute values are stored separately:

```php
metric('page_views')->with(['source' => 'google'])->record(); // Creates metric #1
metric('page_views')->with(['source' => 'facebook'])->record(); // Creates metric #2
metric('page_views')->with(['source' => 'google'])->record(); // Increments metric #1
metric('page:views')->with(['source' => 'google'])->record(); // Creates metric #1
metric('page:views')->with(['source' => 'facebook'])->record(); // Creates metric #2
metric('page:views')->with(['source' => 'google'])->record(); // Increments metric #1
```

This allows you to segment and analyze metrics by any dimension:

```php
// Get page views by source
$googleViews = Metric::where('name', 'page_views')
$googleViews = Metric::where('name', 'page:views')
->where('source', 'google')
->sum('value');

Expand All @@ -322,7 +379,7 @@ $conversions = Metric::thisMonth()

// Get mobile vs desktop traffic
$mobileViews = Metric::today()
->where('name', 'page_views')
->where('name', 'page:views')
->where('device', 'mobile')
->sum('value');
```
Expand All @@ -334,7 +391,7 @@ use DirectoryTree\Metrics\MetricData;
use DirectoryTree\Metrics\Facades\Metrics;

Metrics::record(new MetricData(
name: 'page_views',
name: 'page:views',
additional: [
'source' => 'google',
'country' => 'US',
Expand All @@ -347,7 +404,7 @@ Or with the `PendingMetric` class:
```php
use DirectoryTree\Metrics\PendingMetric;

PendingMetric::make('page_views')
PendingMetric::make('page:views')
->with(['source' => 'google', 'country' => 'US'])
->record();
```
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('metrics', function (Blueprint $table) {
$table->unsignedTinyInteger('hour')->nullable()->after('day');
});
}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('metrics', function (Blueprint $table) {
$table->dropColumn('hour');
});
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('metrics', function (Blueprint $table) {
$table->dropIndex(['year', 'month', 'day']);
$table->index(['year', 'month', 'day', 'hour']);
});
}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('metrics', function (Blueprint $table) {
$table->dropIndex(['year', 'month', 'day', 'hour']);
$table->index(['year', 'month', 'day']);
});
}
};
11 changes: 9 additions & 2 deletions src/ArrayMetricRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,15 @@ public function add(Measurable $metric): void
$metric->name(),
$metric->category(),
$existing->value() + $metric->value(),
CarbonImmutable::create($existing->year(), $existing->month(), $existing->day()),
$metric->measurable()
CarbonImmutable::create(
$existing->year(),
$existing->month(),
$existing->day(),
$existing->hour() ?? 0
),
$metric->measurable(),
$metric->additional(),
$existing->hour() !== null
);
} else {
$this->metrics[$key] = $metric;
Expand Down
1 change: 1 addition & 0 deletions src/Jobs/RecordMetric.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public function handle(): void
'year' => $metric->year(),
'month' => $metric->month(),
'day' => $metric->day(),
'hour' => $metric->hour(),
'measurable_type' => $metric->measurable()?->getMorphClass(),
'measurable_id' => $metric->measurable()?->getKey(),
], ['value' => 0]);
Expand Down
5 changes: 4 additions & 1 deletion src/JsonMeasurableEncoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public function encode(Measurable $metric): string
'year' => $metric->year(),
'month' => $metric->month(),
'day' => $metric->day(),
'hour' => $metric->hour(),
'measurable' => $model ? get_class($model) : null,
'measurable_key' => $model?->getKeyName() ?? null,
'measurable_id' => $model?->getKey() ?? null,
Expand All @@ -46,7 +47,8 @@ public function decode(string $key, int $value): Measurable
$date = CarbonImmutable::create(
$attributes['year'],
$attributes['month'],
$attributes['day']
$attributes['day'],
$attributes['hour'] ?? 0
);

return new MetricData(
Expand All @@ -56,6 +58,7 @@ public function decode(string $key, int $value): Measurable
date: $date,
measurable: $model,
additional: $attributes['additional'] ?? [],
hourly: $attributes['hour'] ?? false,
);
}
}
5 changes: 5 additions & 0 deletions src/Measurable.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ public function month(): int;
*/
public function day(): int;

/**
* Get the hour of the metric.
*/
public function hour(): ?int;

/**
* Get the measurable model of the metric.
*/
Expand Down
1 change: 1 addition & 0 deletions src/Metric.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ protected function casts(): array
'year' => 'integer',
'month' => 'integer',
'day' => 'integer',
'hour' => 'integer',
'value' => 'integer',
];
}
Expand Down
38 changes: 38 additions & 0 deletions src/MetricBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,22 @@ public function yesterday(): self
);
}

/**
* Get metrics for this hour.
*/
public function thisHour(): self
{
return $this->onDateTime(now());
}

/**
* Get metrics for last hour.
*/
public function lastHour(): self
{
return $this->onDateTime(now()->subHour());
}

/**
* Get metrics for this week.
*/
Expand Down Expand Up @@ -160,6 +176,20 @@ public function betweenDates(CarbonInterface $start, CarbonInterface $end): self
);
}

/**
* Get metrics between two datetimes (including hours).
*/
public function betweenDateTimes(CarbonInterface $start, CarbonInterface $end): self
{
return $this->whereRaw(
'(year, month, day, hour) >= (?, ?, ?, ?) AND (year, month, day, hour) <= (?, ?, ?, ?)',
[
$start->year, $start->month, $start->day, $start->hour,
$end->year, $end->month, $end->day, $end->hour,
]
);
}

/**
* Get metrics on a specific date.
*/
Expand All @@ -172,4 +202,12 @@ public function onDate(CarbonInterface $date): self
->where('day', $date->day);
});
}

/**
* Get metrics on a specific date and hour.
*/
public function onDateTime(CarbonInterface $hour): self
{
return $this->onDate($hour)->where('hour', $hour->hour);
}
}
9 changes: 9 additions & 0 deletions src/MetricData.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public function __construct(
protected ?CarbonInterface $date = null,
protected ?Model $measurable = null,
protected array $additional = [],
protected bool $hourly = false,
) {
$this->date ??= new CarbonImmutable;
}
Expand Down Expand Up @@ -74,6 +75,14 @@ public function day(): int
return $this->date->day;
}

/**
* {@inheritDoc}
*/
public function hour(): ?int
{
return $this->hourly ? $this->date->hour : null;
}

/**
* {@inheritDoc}
*/
Expand Down
1 change: 1 addition & 0 deletions src/MetricFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public function definition(): array
'year' => $datetime->year,
'month' => $datetime->month,
'day' => $datetime->day,
'hour' => null,
'measurable_type' => null,
'measurable_id' => null,
'value' => 1,
Expand Down
18 changes: 17 additions & 1 deletion src/PendingMetric.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ class PendingMetric
*/
protected ?Model $measurable = null;

/**
* Whether to track hourly metrics.
*/
protected bool $trackHourly = false;

/**
* Additional attributes to store with the metric.
*
Expand Down Expand Up @@ -81,6 +86,16 @@ public function measurable(Model $measurable): self
return $this;
}

/**
* Enable hourly tracking for the metric.
*/
public function hourly(): self
{
$this->trackHourly = true;

return $this;
}

/**
* Set additional attributes to store with the metric.
*
Expand Down Expand Up @@ -114,7 +129,8 @@ public function toMetricData(int $value): Measurable
$value,
$this->date,
$this->measurable,
$this->additional
$this->additional,
$this->trackHourly
);
}
}
Loading