diff --git a/config/pulse.php b/config/pulse.php index d2297b24..3b07ae4d 100644 --- a/config/pulse.php +++ b/config/pulse.php @@ -209,9 +209,9 @@ */ 'cache_keys' => [ - '^post:139$' => 'Post 139', - '^server:1\d{2}$' => 'Servers 100 - 199', - '^flight:.*' => 'All flights', + // '/^user:.\d+:(.*)/' => 'user:*:\1', + // '/^user:.+$/' => 'user:*', + '/(.*)/' => '\1', ], /* diff --git a/database/migrations/2023_06_07_000001_create_pulse_tables.php b/database/migrations/2023_06_07_000001_create_pulse_tables.php index 9735e112..0b49c541 100644 --- a/database/migrations/2023_06_07_000001_create_pulse_tables.php +++ b/database/migrations/2023_06_07_000001_create_pulse_tables.php @@ -82,7 +82,7 @@ public function up(): void $table->string('key'); $table->boolean('hit'); $table->string('user_id')->nullable(); - // TODO: indexes? + $table->index(['date', 'key']); }); Schema::create('pulse_outgoing_requests', function (Blueprint $table) { diff --git a/resources/views/livewire/cache.blade.php b/resources/views/livewire/cache.blade.php index 875c2f6d..c1b6d14e 100644 --- a/resources/views/livewire/cache.blade.php +++ b/resources/views/livewire/cache.blade.php @@ -1,7 +1,7 @@ @@ -25,39 +25,35 @@ }" :class="[loadingNewDataset ? 'opacity-25 animate-pulse' : '', 'space-y-6']" > -
-
- - {{ number_format($allCacheInteractions->hits) }} - - - Hits - -
-
- - {{ number_format($allCacheInteractions->count - $allCacheInteractions->hits) }} - - - Misses - -
-
- - {{ $allCacheInteractions->count > 0 ? round(($allCacheInteractions->hits / $allCacheInteractions->count) * 100, 2).'%' : '-' }} - - - Hit Rate - -
-
- @if ($monitoredCacheInteractions->isEmpty()) -
-
- No keys configured to monitor + @if ($allCacheInteractions->count === 0) + + @else +
+
+ + {{ number_format($allCacheInteractions->hits) }} + + + Hits + +
+
+ + {{ number_format($allCacheInteractions->count - $allCacheInteractions->hits) }} + + + Misses + +
+
+ + {{ $allCacheInteractions->count > 0 ? round(($allCacheInteractions->hits / $allCacheInteractions->count) * 100, 2).'%' : '-' }} + + + Hit Rate +
- @else @@ -67,14 +63,14 @@ - Name + Key Hits Misses Hit Rate - @foreach ($monitoredCacheInteractions as $interaction) + @foreach ($cacheKeyInteractions->take(100) as $interaction) diff --git a/src/Livewire/Cache.php b/src/Livewire/Cache.php index b7fb647a..388d8bc7 100644 --- a/src/Livewire/Cache.php +++ b/src/Livewire/Cache.php @@ -9,6 +9,7 @@ use Laravel\Pulse\Livewire\Concerns\RemembersQueries; use Laravel\Pulse\Livewire\Concerns\ShouldNotReportUsage; use Laravel\Pulse\Queries\CacheInteractions; +use Laravel\Pulse\Queries\CacheKeyInteractions; use Laravel\Pulse\Queries\MonitoredCacheInteractions; use Livewire\Attributes\Lazy; @@ -20,27 +21,19 @@ class Cache extends Card /** * Render the component. */ - public function render(CacheInteractions $cacheInteractionsQuery, MonitoredCacheInteractions $monitoredCacheInteractionsQuery): Renderable + public function render(CacheInteractions $cacheInteractionsQuery, CacheKeyInteractions $cacheKeyInteractionsQuery): Renderable { - $monitoring = collect(Config::get('pulse.cache_keys')) - ->mapWithKeys(fn (string $value, int|string $key) => is_string($key) - ? [$key => $value] - : [$value => $value]); + [$cacheInteractions, $allTime, $allRunAt] = $this->remember($cacheInteractionsQuery, 'all'); - [$cacheInteractions, $allTime, $allRunAt] = $this->remember($cacheInteractionsQuery); - - [$monitoredCacheInteractions, $monitoredTime, $monitoredRunAt] = $this->remember( - fn ($interval) => $monitoredCacheInteractionsQuery($interval, $monitoring), - md5($monitoring->toJson()) - ); + [$cacheKeyInteractions, $keyTime, $keyRunAt] = $this->remember($cacheKeyInteractionsQuery, 'keys'); return View::make('pulse::livewire.cache', [ 'allTime' => $allTime, 'allRunAt' => $allRunAt, - 'monitoredTime' => $monitoredTime, - 'monitoredRunAt' => $monitoredRunAt, 'allCacheInteractions' => $cacheInteractions, - 'monitoredCacheInteractions' => $monitoredCacheInteractions, + 'keyTime' => $keyTime, + 'keyRunAt' => $keyRunAt, + 'cacheKeyInteractions' => $cacheKeyInteractions, ]); } } diff --git a/src/Queries/CacheInteractions.php b/src/Queries/CacheInteractions.php index ab02178b..9d24827e 100644 --- a/src/Queries/CacheInteractions.php +++ b/src/Queries/CacheInteractions.php @@ -31,13 +31,9 @@ public function __invoke(Interval $interval): object { $now = new CarbonImmutable(); - $cacheInteractions = $this->connection()->table('pulse_cache_interactions') + return $this->connection()->table('pulse_cache_interactions') ->selectRaw('COUNT(*) AS count, SUM(CASE WHEN `hit` = TRUE THEN 1 ELSE 0 END) as hits') - ->where('date', '>=', $now->subSeconds((int) $interval->totalSeconds)->toDateTimeString()) - ->first() ?? (object) ['hits' => 0]; - - $cacheInteractions->hits = (int) $cacheInteractions->hits; - - return $cacheInteractions; + ->where('date', '>', $now->subSeconds((int) $interval->totalSeconds)->toDateTimeString()) + ->first() ?? (object) ['count' => 0, 'hits' => '0']; } } diff --git a/src/Queries/CacheKeyInteractions.php b/src/Queries/CacheKeyInteractions.php new file mode 100644 index 00000000..f2435d29 --- /dev/null +++ b/src/Queries/CacheKeyInteractions.php @@ -0,0 +1,47 @@ + + */ + public function __invoke(Interval $interval): Collection + { + $now = new CarbonImmutable(); + + return $this->connection()->table('pulse_cache_interactions') + ->selectRaw('`key`, COUNT(*) AS count, SUM(CASE WHEN `hit` = TRUE THEN 1 ELSE 0 END) as hits') + ->where('date', '>', $now->subSeconds((int) $interval->totalSeconds)->toDateTimeString()) + ->groupBy('key') + ->orderByDesc('count') + ->limit(101) + ->get(); + } +} diff --git a/src/Queries/Exceptions.php b/src/Queries/Exceptions.php index 56afdfd7..1c1918e4 100644 --- a/src/Queries/Exceptions.php +++ b/src/Queries/Exceptions.php @@ -37,7 +37,7 @@ public function __invoke(Interval $interval, string $orderBy): Collection return $this->connection()->table('pulse_exceptions') ->selectRaw('class, location, COUNT(*) AS count, MAX(date) AS last_occurrence') - ->where('date', '>=', $now->subSeconds((int) $interval->totalSeconds)->toDateTimeString()) + ->where('date', '>', $now->subSeconds((int) $interval->totalSeconds)->toDateTimeString()) ->groupBy('class', 'location') ->orderByDesc($orderBy) ->get(); diff --git a/src/Queries/MonitoredCacheInteractions.php b/src/Queries/MonitoredCacheInteractions.php deleted file mode 100644 index 724ba4c8..00000000 --- a/src/Queries/MonitoredCacheInteractions.php +++ /dev/null @@ -1,80 +0,0 @@ - $keys - * @return \Illuminate\Support\Collection - */ - public function __invoke(Interval $interval, Collection $keys): Collection - { - if ($keys->isEmpty()) { - return collect([]); - } - - $now = new CarbonImmutable(); - - $interactions = $keys->mapWithKeys(fn (string $name, string $regex) => [ - $name => (object) [ - 'regex' => $regex, - 'key' => $name, - 'uniqueKeys' => 0, - 'hits' => 0, - 'count' => 0, - ], - ]); - - $this->connection()->table('pulse_cache_interactions') - ->selectRaw('`key`, COUNT(*) AS count, SUM(CASE WHEN `hit` = TRUE THEN 1 ELSE 0 END) as hits') - ->where('date', '>=', $now->subSeconds((int) $interval->totalSeconds)->toDateTimeString()) - // TODO: ensure PHP and MySQL regex is compatible - // TODO modifiers? is redis / memcached / etc case sensitive? - ->where(fn (Builder $query) => $keys->keys()->each(fn (string $key) => $query->orWhere('key', 'RLIKE', $key))) - ->orderBy('key') - ->groupBy('key') - ->each(function (stdClass $result) use ($interactions, $keys) { - $name = $keys->firstWhere(fn (string $name, string $regex) => preg_match('/'.$regex.'/', $result->key) > 0); - - if ($name === null) { - return; - } - - $interaction = $interactions[$name]; - - $interaction->uniqueKeys++; - $interaction->hits += $result->hits; - $interaction->count += $result->count; - }); - - $monitoringIndex = $keys->values()->flip(); - - return $interactions->sortBy(fn (stdClass $interaction) => $monitoringIndex[$interaction->key]); - } -} diff --git a/src/Queries/Servers.php b/src/Queries/Servers.php index 4ba55dc4..b7659114 100644 --- a/src/Queries/Servers.php +++ b/src/Queries/Servers.php @@ -71,7 +71,7 @@ public function __invoke(Interval $interval): Collection ->select(['server', 'cpu_percent', 'memory_used', 'date']) // Divide the data into buckets. ->selectRaw('FLOOR(UNIX_TIMESTAMP(CONVERT_TZ(`date`, ?, @@session.time_zone)) / ?) AS `bucket`', [$now->format('P'), $secondsPerPeriod]) - ->where('date', '>=', $now->ceilSeconds($interval->totalSeconds / $maxDataPoints)->subSeconds((int) $interval->totalSeconds)), + ->where('date', '>', $now->ceilSeconds($interval->totalSeconds / $maxDataPoints)->subSeconds((int) $interval->totalSeconds)), 'grouped' ) ->groupBy('server', 'bucket') diff --git a/src/Queries/SlowJobs.php b/src/Queries/SlowJobs.php index 380b3fdf..c14c6939 100644 --- a/src/Queries/SlowJobs.php +++ b/src/Queries/SlowJobs.php @@ -36,7 +36,7 @@ public function __invoke(Interval $interval): Collection return $this->connection()->table('pulse_jobs') ->selectRaw('`job`, SUM(slow) as count, MAX(slowest) as slowest') - ->where('date', '>=', $now->subSeconds((int) $interval->totalSeconds)->toDateTimeString()) + ->where('date', '>', $now->subSeconds((int) $interval->totalSeconds)->toDateTimeString()) ->where('slow', '>', 0) ->groupBy('job') ->orderByDesc('slowest') diff --git a/src/Queries/SlowOutgoingRequests.php b/src/Queries/SlowOutgoingRequests.php index 660483a2..4a8a5c55 100644 --- a/src/Queries/SlowOutgoingRequests.php +++ b/src/Queries/SlowOutgoingRequests.php @@ -36,7 +36,7 @@ public function __invoke(Interval $interval): Collection return $this->connection()->table('pulse_outgoing_requests') ->selectRaw('`uri`, COUNT(*) as count, MAX(duration) AS slowest') - ->where('date', '>=', $now->subSeconds((int) $interval->totalSeconds)->toDateTimeString()) + ->where('date', '>', $now->subSeconds((int) $interval->totalSeconds)->toDateTimeString()) ->where('duration', '>=', $this->config->get('pulse.slow_outgoing_request_threshold')) ->groupBy('uri') ->orderByDesc('slowest') diff --git a/src/Queries/SlowQueries.php b/src/Queries/SlowQueries.php index e100b139..74d958a2 100644 --- a/src/Queries/SlowQueries.php +++ b/src/Queries/SlowQueries.php @@ -36,7 +36,7 @@ public function __invoke(Interval $interval): Collection return $this->connection()->table('pulse_slow_queries') ->selectRaw('`sql`, COUNT(*) as count, MAX(duration) AS slowest') - ->where('date', '>=', $now->subSeconds((int) $interval->totalSeconds)->toDateTimeString()) + ->where('date', '>', $now->subSeconds((int) $interval->totalSeconds)->toDateTimeString()) ->groupBy('sql') ->orderByDesc('slowest') ->get(); diff --git a/src/Queries/SlowRoutes.php b/src/Queries/SlowRoutes.php index 1dca11fb..c327b5aa 100644 --- a/src/Queries/SlowRoutes.php +++ b/src/Queries/SlowRoutes.php @@ -39,7 +39,7 @@ public function __invoke(Interval $interval): Collection return $this->connection()->table('pulse_requests') ->selectRaw('route, COUNT(*) as count, MAX(duration) AS slowest') - ->where('date', '>=', $now->subSeconds((int) $interval->totalSeconds)->toDateTimeString()) + ->where('date', '>', $now->subSeconds((int) $interval->totalSeconds)->toDateTimeString()) ->where('duration', '>=', $this->config->get('pulse.slow_endpoint_threshold')) ->groupBy('route') ->orderByDesc('slowest') diff --git a/src/Queries/Usage.php b/src/Queries/Usage.php index 830fa440..32803025 100644 --- a/src/Queries/Usage.php +++ b/src/Queries/Usage.php @@ -44,7 +44,7 @@ public function __invoke(Interval $interval, string $type): Collection fn (Builder $query) => $query->from('pulse_requests')) ->selectRaw('user_id, COUNT(*) as count') ->whereNotNull('user_id') - ->where('date', '>=', $now->subSeconds((int) $interval->totalSeconds)->toDateTimeString()) + ->where('date', '>', $now->subSeconds((int) $interval->totalSeconds)->toDateTimeString()) ->when($type === 'slow_endpoint_counts', fn (Builder $query) => $query->where('duration', '>=', $this->config->get('pulse.slow_endpoint_threshold'))) ->groupBy('user_id') diff --git a/src/Recorders/CacheInteractions.php b/src/Recorders/CacheInteractions.php index 3dae64bb..39c37ef2 100644 --- a/src/Recorders/CacheInteractions.php +++ b/src/Recorders/CacheInteractions.php @@ -3,8 +3,10 @@ namespace Laravel\Pulse\Recorders; use Carbon\CarbonImmutable; +use Closure; use Illuminate\Cache\Events\CacheHit; use Illuminate\Cache\Events\CacheMissed; +use Illuminate\Config\Repository; use Illuminate\Support\Str; use Laravel\Pulse\Entry; use Laravel\Pulse\Pulse; @@ -34,6 +36,7 @@ class CacheInteractions */ public function __construct( protected Pulse $pulse, + protected Repository $config, ) { // } @@ -45,15 +48,35 @@ public function record(CacheHit|CacheMissed $event): ?Entry { $now = new CarbonImmutable(); - if (Str::startsWith($event->key, ['illuminate:', 'laravel:pulse'])) { + if (Str::startsWith($event->key, ['illuminate:', 'laravel:pulse:'])) { return null; } return new Entry($this->table, [ 'date' => $now->toDateTimeString(), 'hit' => $event instanceof CacheHit, - 'key' => $event->key, + 'key' => $this->normalizeKey($event), 'user_id' => $this->pulse->authenticatedUserIdResolver(), ]); } + + /** + * Normalize the cache key. + */ + protected function normalizeKey(CacheHit|CacheMissed $event): Closure + { + $key = $event->key; + + return function () use ($key) { + foreach ($this->config->get('pulse.cache_keys') as $pattern => $replacement) { + $normalized = preg_replace($pattern, $replacement, $key, count: $count); + + if ($count > 0 && $normalized !== null) { + return $normalized; + } + } + + return $key; + }; + } } diff --git a/tests/Feature/CacheInteractionsTest.php b/tests/Feature/CacheInteractionsTest.php index bfd0399d..be298cf5 100644 --- a/tests/Feature/CacheInteractionsTest.php +++ b/tests/Feature/CacheInteractionsTest.php @@ -20,16 +20,11 @@ Carbon::setTestNow('2000-01-02 03:04:05'); Cache::get('cache-key'); - - expect(Pulse::entries())->toHaveCount(1); - Pulse::ignore(fn () => expect(DB::table('pulse_cache_interactions')->count())->toBe(0)); - Pulse::store(app(Ingest::class)); - expect(Pulse::entries())->toHaveCount(0); - $cacheHits = Pulse::ignore(fn () => DB::table('pulse_cache_interactions')->get()); - expect($cacheHits)->toHaveCount(1); - expect((array) $cacheHits[0])->toEqual([ + $interactions = Pulse::ignore(fn () => DB::table('pulse_cache_interactions')->get()); + expect($interactions)->toHaveCount(1); + expect($interactions[0])->toHaveProperties([ 'date' => '2000-01-02 03:04:05', 'user_id' => null, 'key' => 'cache-key', @@ -37,43 +32,39 @@ ]); }); -it('ignores any internal illuminate cache interactions', function () { - Carbon::setTestNow('2000-01-02 03:04:05'); - +it('ignores internal illuminate cache interactions', function () { Cache::get('illuminate:'); Pulse::store(app(Ingest::class)); - $cacheHits = Pulse::ignore(fn () => DB::table('pulse_cache_interactions')->get()); - expect($cacheHits)->toHaveCount(0); + $interactions = Pulse::ignore(fn () => DB::table('pulse_cache_interactions')->get()); + expect($interactions)->toHaveCount(0); }); -it('ignores any internal pulse cache interactions', function () { - Carbon::setTestNow('2000-01-02 03:04:05'); - - Cache::get('laravel:pulse'); +it('ignores internal pulse cache interactions', function () { + Cache::get('laravel:pulse:foo'); Pulse::store(app(Ingest::class)); - $cacheHits = Pulse::ignore(fn () => DB::table('pulse_cache_interactions')->get()); - expect($cacheHits)->toHaveCount(0); + $interactions = Pulse::ignore(fn () => DB::table('pulse_cache_interactions')->get()); + expect($interactions)->toHaveCount(0); }); -it('captures hits and misses', function () { +it('ingests hits', function () { Carbon::setTestNow('2000-01-02 03:04:05'); - Cache::put('hit', 123); + Cache::put('hit', 123); Cache::get('hit'); Cache::get('miss'); Pulse::store(app(Ingest::class)); - $cacheHits = Pulse::ignore(fn () => DB::table('pulse_cache_interactions')->get()); - expect($cacheHits)->toHaveCount(2); - expect((array) $cacheHits[0])->toEqual([ + $interactions = Pulse::ignore(fn () => DB::table('pulse_cache_interactions')->get()); + expect($interactions)->toHaveCount(2); + expect($interactions[0])->toHaveProperties([ 'date' => '2000-01-02 03:04:05', 'user_id' => null, 'key' => 'hit', 'hit' => 1, ]); - expect((array) $cacheHits[1])->toEqual([ + expect($interactions[1])->toHaveProperties([ 'date' => '2000-01-02 03:04:05', 'user_id' => null, 'key' => 'miss', @@ -87,9 +78,9 @@ Cache::get('cache-key'); Pulse::store(app(Ingest::class)); - $cacheHits = Pulse::ignore(fn () => DB::table('pulse_cache_interactions')->get()); - expect($cacheHits)->toHaveCount(1); - expect($cacheHits[0]->user_id)->toBe('567'); + $interactions = Pulse::ignore(fn () => DB::table('pulse_cache_interactions')->get()); + expect($interactions)->toHaveCount(1); + expect($interactions[0]->user_id)->toBe('567'); }); it('captures the authenticated user if they login after the interaction', function () { @@ -97,9 +88,9 @@ Auth::login(User::make(['id' => '567'])); Pulse::store(app(Ingest::class)); - $cacheHits = Pulse::ignore(fn () => DB::table('pulse_cache_interactions')->get()); - expect($cacheHits)->toHaveCount(1); - expect($cacheHits[0]->user_id)->toBe('567'); + $interactions = Pulse::ignore(fn () => DB::table('pulse_cache_interactions')->get()); + expect($interactions)->toHaveCount(1); + expect($interactions[0]->user_id)->toBe('567'); }); it('captures the authenticated user if they logout after the interaction', function () { @@ -109,9 +100,9 @@ Auth::logout(); Pulse::store(app(Ingest::class)); - $cacheHits = Pulse::ignore(fn () => DB::table('pulse_cache_interactions')->get()); - expect($cacheHits)->toHaveCount(1); - expect($cacheHits[0]->user_id)->toBe('567'); + $interactions = Pulse::ignore(fn () => DB::table('pulse_cache_interactions')->get()); + expect($interactions)->toHaveCount(1); + expect($interactions[0]->user_id)->toBe('567'); }); it('does not trigger an inifite loop when retriving the authenticated user from the database', function () { @@ -140,9 +131,9 @@ public function user() Cache::get('cache-key'); Pulse::store(app(Ingest::class)); - $cacheHits = Pulse::ignore(fn () => DB::table('pulse_cache_interactions')->get()); - expect($cacheHits)->toHaveCount(1); - expect($cacheHits[0]->user_id)->toBe(null); + $interactions = Pulse::ignore(fn () => DB::table('pulse_cache_interactions')->get()); + expect($interactions)->toHaveCount(1); + expect($interactions[0]->user_id)->toBe(null); }); it('quietly fails if an exception is thrown while preparing the entry payload', function () { @@ -178,3 +169,96 @@ public function hasUser() expect($interactions[1]->user_id)->toBe('567'); expect($interactions[2]->user_id)->toBe('789'); }); + +it('stores the original keys by default', function () { + Carbon::setTestNow('2000-01-02 03:04:05'); + + Cache::get('users:1234:profile'); + Pulse::store(app(Ingest::class)); + + $interactions = Pulse::ignore(fn () => DB::table('pulse_cache_interactions')->get()); + expect($interactions)->toHaveCount(1); + expect((array) $interactions[0])->toEqual([ + 'date' => '2000-01-02 03:04:05', + 'user_id' => null, + 'key' => 'users:1234:profile', + 'hit' => 0, + ]); +}); + +it('can normalize cache keys', function () { + Carbon::setTestNow('2000-01-02 03:04:05'); + + Config::set('pulse.cache_keys', [ + '/users:\d+:profile/' => 'users:{user}:profile', + ]); + Cache::get('users:1234:profile'); + Pulse::store(app(Ingest::class)); + + $interactions = Pulse::ignore(fn () => DB::table('pulse_cache_interactions')->get()); + expect($interactions)->toHaveCount(1); + expect((array) $interactions[0])->toEqual([ + 'date' => '2000-01-02 03:04:05', + 'user_id' => null, + 'key' => 'users:{user}:profile', + 'hit' => 0, + ]); +}); + +it('can use back references in normalized cache keys', function () { + Carbon::setTestNow('2000-01-02 03:04:05'); + + Config::set('pulse.cache_keys', [ + '/^([^:]+):([^:]+):baz/' => '\2:\1', + ]); + Cache::get('foo:bar:baz'); + Pulse::store(app(Ingest::class)); + + $interactions = Pulse::ignore(fn () => DB::table('pulse_cache_interactions')->get()); + expect($interactions)->toHaveCount(1); + expect((array) $interactions[0])->toEqual([ + 'date' => '2000-01-02 03:04:05', + 'user_id' => null, + 'key' => 'bar:foo', + 'hit' => 0, + ]); +}); + +it('uses the original key if no matching pattern is found', function () { + Carbon::setTestNow('2000-01-02 03:04:05'); + + Config::set('pulse.cache_keys', [ + '/\d/' => 'foo', + ]); + Cache::get('actual-key'); + Pulse::store(app(Ingest::class)); + + $interactions = Pulse::ignore(fn () => DB::table('pulse_cache_interactions')->get()); + expect($interactions)->toHaveCount(1); + expect((array) $interactions[0])->toEqual([ + 'date' => '2000-01-02 03:04:05', + 'user_id' => null, + 'key' => 'actual-key', + 'hit' => 0, + ]); +}); + +it('can provide regex flags in normalization key', function () { + Carbon::setTestNow('2000-01-02 03:04:05'); + + Config::set('pulse.cache_keys', [ + '/foo/i' => 'lowercase-key', + '/FOO/i' => 'uppercase-key', + ]); + Cache::get('FOO'); + Pulse::store(app(Ingest::class)); + + $interactions = Pulse::ignore(fn () => DB::table('pulse_cache_interactions')->get()); + expect($interactions)->toHaveCount(1); + expect((array) $interactions[0])->toEqual([ + 'date' => '2000-01-02 03:04:05', + 'user_id' => null, + 'key' => 'lowercase-key', + 'hit' => 0, + ]); +}); diff --git a/tests/Feature/Queries/CacheKeyInteractionsTest.php b/tests/Feature/Queries/CacheKeyInteractionsTest.php new file mode 100644 index 00000000..410735f4 --- /dev/null +++ b/tests/Feature/Queries/CacheKeyInteractionsTest.php @@ -0,0 +1,105 @@ +insert([ + [ + 'date' => $date = now()->toDateTimeString(), + 'hit' => true, + 'key' => 'users:{user}:avatar' + ], + [ + 'date' => $date, + 'hit' => true, + 'key' => 'users:{user}:profile' + ], + [ + 'date' => $date, + 'hit' => true, + 'key' => 'users:{user}:profile' + ], + [ + 'date' => $date, + 'hit' => true, + 'key' => 'users:{user}:profile' + ], + [ + 'date' => $date, + 'hit' => false, + 'key' => 'users:{user}:avatar' + ], + [ + 'date' => $date, + 'hit' => false, + 'key' => 'users:{user}:avatar' + ], + [ + 'date' => $date, + 'hit' => false, + 'key' => 'users:{user}:avatar' + ], + [ + 'date' => $date = now()->toDateTimeString(), + 'hit' => true, + 'key' => 'users:{user}:avatar' + ], + ]); + + $results = $query(CarbonInterval::hour()); + + expect($results)->toHaveCount(2); + expect($results[0])->toHaveProperties([ + 'key' => 'users:{user}:avatar', + 'count' => 5, + 'hits' => "2", + ]); + expect($results[1])->toHaveProperties([ + 'key' => 'users:{user}:profile', + 'count' => 3, + 'hits' => "3", + ]); +}); + +it('contains records to interval', function () { + $query = App::make(CacheKeyInteractions::class); + DB::table('pulse_cache_interactions')->insert([ + [ + 'date' => '2000-01-01 00:00:05', + 'hit' => true, + 'key' => 'before' + ], + [ + 'date' => '2000-01-01 00:00:06', + 'hit' => true, + 'key' => 'after' + ], + ]); + Carbon::setTestNow('2000-01-01 01:00:05'); + + $results = $query(CarbonInterval::hour()); + + expect($results)->toHaveCount(1); + expect($results[0]->key)->toBe('after'); +}); + +it('limits to 101 records', function () { + $query = App::make(CacheKeyInteractions::class); + for ($i = 0; $i < 200; $i++) { + DB::table('pulse_cache_interactions')->insert([ + 'date' => now()->toDateTimeString(), + 'hit' => true, + 'key' => Str::random(), + ]); + } + + $results = $query(CarbonInterval::hour()); + + expect($results)->toHaveCount(101); +});