From 06b8e35de8de58d550e3d1d7b75dbd75ced7ef38 Mon Sep 17 00:00:00 2001 From: Eser DENIZ Date: Fri, 1 Dec 2023 18:45:01 +0000 Subject: [PATCH 001/110] Added Nova keys on default cache config (#79) Ex: nova.version, nova_valid_license_key --- config/pulse.php | 1 + 1 file changed, 1 insertion(+) diff --git a/config/pulse.php b/config/pulse.php index fbfc06e6..ed66d707 100644 --- a/config/pulse.php +++ b/config/pulse.php @@ -134,6 +134,7 @@ '/^laravel:pulse:/', // Internal Pulse keys... '/^illuminate:/', // Internal Laravel keys... '/^telescope:/', // Internal Telescope keys... + '/^nova/', // Internal Nova keys... '/^.+@.+\|(?:(?:\d+\.\d+\.\d+\.\d+)|[0-9a-fA-F:]+)(?::timer)?$/', // Breeze / Jetstream authentication rate limiting... '/^[a-zA-Z0-9]{40}$/', // Session IDs... ], From c8a6e1126fa343b5a752cb1226ba60fd94c02cb8 Mon Sep 17 00:00:00 2001 From: woodspire Date: Fri, 1 Dec 2023 13:45:37 -0500 Subject: [PATCH 002/110] Update Pulse.php (#76) Fix problem doing "composer require laravel/pulse" Uncaught TypeError: Laravel\Pulse\Pulse::Laravel\Pulse\{closure}(): Argument #1 ($event) must be of type Illumi nate\Events\Dispatcher, Illuminate\Events\NullDispatcher given, called in /var/www/sapere/sapere/vendor/laravel/framework/src/Illuminate/Container/Container.php on line 1302 and defined in /var/www/sapere/sapere/vendor/laravel/pulse/src/Pulse.php:119 Stack trace: --- src/Pulse.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Pulse.php b/src/Pulse.php index 70927053..aace54a9 100644 --- a/src/Pulse.php +++ b/src/Pulse.php @@ -6,7 +6,7 @@ use DateTimeInterface; use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Contracts\Foundation\Application; -use Illuminate\Events\Dispatcher; +use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Support\Collection; use Illuminate\Support\Lottery; use Illuminate\Support\Traits\ForwardsCalls; From fd8411cfb27047e576d37c7b00655da279c8665a Mon Sep 17 00:00:00 2001 From: taylorotwell Date: Fri, 1 Dec 2023 18:45:58 +0000 Subject: [PATCH 003/110] Fix code styling --- src/Pulse.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Pulse.php b/src/Pulse.php index aace54a9..8c079e37 100644 --- a/src/Pulse.php +++ b/src/Pulse.php @@ -5,8 +5,8 @@ use Carbon\CarbonImmutable; use DateTimeInterface; use Illuminate\Contracts\Auth\Authenticatable; -use Illuminate\Contracts\Foundation\Application; use Illuminate\Contracts\Events\Dispatcher; +use Illuminate\Contracts\Foundation\Application; use Illuminate\Support\Collection; use Illuminate\Support\Lottery; use Illuminate\Support\Traits\ForwardsCalls; From 1de2604de554c6a0c8c29f706615b4e30995628d Mon Sep 17 00:00:00 2001 From: Willy Reyno Date: Fri, 1 Dec 2023 19:46:10 +0100 Subject: [PATCH 004/110] Fix Class "Str" not found error on fresh install (#74) * Add Illuminate\Support\Str import to queues.blade.php * Add Illuminate\Support\Str import to slow-outgoing-requests.blade.php * Add Illuminate\Support\Str import to cache.blade.php --- resources/views/livewire/cache.blade.php | 3 +++ resources/views/livewire/queues.blade.php | 3 +++ resources/views/livewire/slow-outgoing-requests.blade.php | 3 +++ 3 files changed, 9 insertions(+) diff --git a/resources/views/livewire/cache.blade.php b/resources/views/livewire/cache.blade.php index b40cc794..ed296564 100644 --- a/resources/views/livewire/cache.blade.php +++ b/resources/views/livewire/cache.blade.php @@ -1,3 +1,6 @@ +@php + use Illuminate\Support\Str; +@endphp Date: Fri, 1 Dec 2023 22:46:10 +0100 Subject: [PATCH 005/110] Update pulse.blade.php (#86) --- resources/views/components/pulse.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/components/pulse.blade.php b/resources/views/components/pulse.blade.php index 3e3f06c0..d3fb08c6 100644 --- a/resources/views/components/pulse.blade.php +++ b/resources/views/components/pulse.blade.php @@ -45,7 +45,7 @@
-
merge(['class' => "mx-auto grid default:grid-cols-{$cols} default:gap-6" . ($fullWidth ? '' : ' container')]) }}"> +
merge(['class' => "mx-auto grid default:grid-cols-{$cols} default:gap-6" . ($fullWidth ? '' : ' container')]) }}> {{ $slot }}
From bb589881e1888a97c2301fbb54b2f3ccc8b3a493 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Fri, 1 Dec 2023 15:58:42 -0600 Subject: [PATCH 006/110] fix precedence --- src/Livewire/Usage.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Livewire/Usage.php b/src/Livewire/Usage.php index d490435d..e9bf4596 100644 --- a/src/Livewire/Usage.php +++ b/src/Livewire/Usage.php @@ -65,9 +65,9 @@ function () use ($type) { 'id' => $row->key, 'name' => $user['name'] ?? 'Unknown', 'extra' => $user['extra'] ?? $user['email'] ?? '', - 'avatar' => $user['avatar'] ?? ($user['email'] ?? false) + 'avatar' => $user['avatar'] ?? (($user['email'] ?? false) ? sprintf('https://gravatar.com/avatar/%s?d=mp', hash('sha256', trim(strtolower($user['email'])))) - : null, + : null), ], 'count' => (int) $row->count, ]; From 3afeb3f59af17e6d496ac77f7b8bdd38384d8902 Mon Sep 17 00:00:00 2001 From: Tobias Oitzinger <42447585+toitzi@users.noreply.github.com> Date: Fri, 1 Dec 2023 23:00:37 +0100 Subject: [PATCH 007/110] Manually inject livewire assets (#88) * Manually inject livewire assets Signed-off-by: Tobias Oitzinger * move scripts to end of body Signed-off-by: Tobias Oitzinger * Update pulse.blade.php --------- Signed-off-by: Tobias Oitzinger Co-authored-by: Taylor Otwell --- resources/views/components/pulse.blade.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/resources/views/components/pulse.blade.php b/resources/views/components/pulse.blade.php index d3fb08c6..b3930042 100644 --- a/resources/views/components/pulse.blade.php +++ b/resources/views/components/pulse.blade.php @@ -15,6 +15,8 @@ {!! Laravel\Pulse\Facades\Pulse::css() !!} + @livewireStyles + @@ -51,6 +53,7 @@ + @livewireScripts @stack('scripts') From c1977d3add46287aad24cb00f8db521f6fb23715 Mon Sep 17 00:00:00 2001 From: Andy Hinkle Date: Fri, 1 Dec 2023 20:36:19 -0600 Subject: [PATCH 008/110] Fix documentation link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8ab4739a..bc498c67 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Laravel Pulse is a real-time application performance monitoring tool and dashboa ## Official Documentation -Documentation for Pulse can be found on the [Laravel website](https://laravel.com/docs). +Documentation for Pulse can be found on the [Laravel website](https://laravel.com/docs/pulse). ## Contributing From acd68f8bebd3bc5a66ff74e421f7fd3a0001c582 Mon Sep 17 00:00:00 2001 From: Rafael Milewski Date: Sat, 2 Dec 2023 10:45:08 +0800 Subject: [PATCH 009/110] dark mode --- art/logo.svg | 73 +++++++++++++++++----------------------------------- 1 file changed, 24 insertions(+), 49 deletions(-) diff --git a/art/logo.svg b/art/logo.svg index 90b2bb78..68ae1a34 100644 --- a/art/logo.svg +++ b/art/logo.svg @@ -1,51 +1,26 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + From d23050848ddb1ac6d176a9ae676a760df9743036 Mon Sep 17 00:00:00 2001 From: Andy Hinkle Date: Sat, 2 Dec 2023 12:21:09 -0600 Subject: [PATCH 010/110] Remove Laravel 9 Support; Require a minimum of Laravel v10.20. (#99) * Remove Laravel v9.0 Support * Require a minimum of Laravel v10.20 --- composer.json | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/composer.json b/composer.json index 24a9eb5b..68115604 100644 --- a/composer.json +++ b/composer.json @@ -18,19 +18,19 @@ "php": "^8.1", "doctrine/sql-formatter": "^1.1", "guzzlehttp/promises": "^1.0 || ^2.0", - "illuminate/auth": "^9.0 || ^10.0", - "illuminate/cache": "^9.0 || ^10.0", - "illuminate/config": "^9.0 || ^10.0", - "illuminate/console": "^9.0 || ^10.0", - "illuminate/contracts": "^9.0 || ^10.0", - "illuminate/database": "^9.0 || ^10.0", - "illuminate/events": "^9.0 || ^10.0", - "illuminate/http": "^9.0 || ^10.0", - "illuminate/queue": "^9.0 || ^10.0", - "illuminate/redis": "^9.0 || ^10.0", - "illuminate/routing": "^9.0 || ^10.0", - "illuminate/support": "^9.0 || ^10.0", - "illuminate/view": "^9.0 || ^10.0", + "illuminate/auth": "^10.20", + "illuminate/cache": "^10.20", + "illuminate/config": "^10.20", + "illuminate/console": "^10.20", + "illuminate/contracts": "^10.20", + "illuminate/database": "^10.20", + "illuminate/events": "^10.20", + "illuminate/http": "^10.20", + "illuminate/queue": "^10.20", + "illuminate/redis": "^10.20", + "illuminate/routing": "^10.20", + "illuminate/support": "^10.20", + "illuminate/view": "^10.20", "livewire/livewire": "^3.0", "nesbot/carbon": "^2.67" }, From 70df32cf8a04f41b5ecaf5dc44fb43833eac7764 Mon Sep 17 00:00:00 2001 From: Andrey Helldar Date: Sat, 2 Dec 2023 21:23:26 +0300 Subject: [PATCH 011/110] Removed duplicate property assignment (#112) --- src/Pulse.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Pulse.php b/src/Pulse.php index 8c079e37..bcd079b1 100644 --- a/src/Pulse.php +++ b/src/Pulse.php @@ -96,7 +96,6 @@ public function __construct(protected Application $app) { $this->filters = collect([]); $this->recorders = collect([]); - $this->recorders = collect([]); $this->entries = collect([]); $this->lazy = collect([]); } From 866b3232e0238fa05fab51f11cdefdf175f6940b Mon Sep 17 00:00:00 2001 From: AJ <60591772+devajmeireles@users.noreply.github.com> Date: Sat, 2 Dec 2023 15:24:18 -0300 Subject: [PATCH 012/110] updating bug_report template (#106) --- .github/ISSUE_TEMPLATE/1_Bug_report.yml | 7 +++++++ .gitignore | 1 + 2 files changed, 8 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/1_Bug_report.yml b/.github/ISSUE_TEMPLATE/1_Bug_report.yml index e879cef3..ebfdb3ef 100644 --- a/.github/ISSUE_TEMPLATE/1_Bug_report.yml +++ b/.github/ISSUE_TEMPLATE/1_Bug_report.yml @@ -25,6 +25,13 @@ body: placeholder: 8.1.4 validations: required: true + - type: input + attributes: + label: Database Driver & Version + description: If applicable, provide the database driver and version you are using. + placeholder: "MySQL 8.0.31 for macOS 13.0 on arm64 (Homebrew)" + validations: + required: false - type: textarea attributes: label: Description diff --git a/.gitignore b/.gitignore index ffaaca47..b32d7d50 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ composer.lock /phpunit.xml .phpunit.result.cache .env +.idea \ No newline at end of file From aca3e5ca9cc21bc9760717c777afbb5bfca24eeb Mon Sep 17 00:00:00 2001 From: Andrey Helldar Date: Sat, 2 Dec 2023 21:25:15 +0300 Subject: [PATCH 013/110] Added IDE projects settings exception (#111) --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index b32d7d50..c852c18c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +/.idea +/.fleet +/.vscode /node_modules /public/app.js.LICENSE.txt /vendor From fca8a98808dc10c3e75d72614233ed913f6b2048 Mon Sep 17 00:00:00 2001 From: Guillaume Date: Sat, 2 Dec 2023 19:29:52 +0100 Subject: [PATCH 014/110] fix mysql engine (#117) --- database/migrations/2023_06_07_000001_create_pulse_tables.php | 3 +++ 1 file changed, 3 insertions(+) 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 2043f141..39141738 100644 --- a/database/migrations/2023_06_07_000001_create_pulse_tables.php +++ b/database/migrations/2023_06_07_000001_create_pulse_tables.php @@ -21,6 +21,7 @@ public function getConnection(): ?string public function up(): void { Schema::create('pulse_values', function (Blueprint $table) { + $table->engine = 'InnoDB'; $table->unsignedInteger('timestamp'); $table->string('type'); $table->text('key'); @@ -33,6 +34,7 @@ public function up(): void }); Schema::create('pulse_entries', function (Blueprint $table) { + $table->engine = 'InnoDB'; $table->unsignedInteger('timestamp'); $table->string('type'); $table->text('key'); @@ -46,6 +48,7 @@ public function up(): void }); Schema::create('pulse_aggregates', function (Blueprint $table) { + $table->engine = 'InnoDB'; $table->unsignedInteger('bucket'); $table->unsignedMediumInteger('period'); $table->string('type'); From 5d3abe624ec80ff53908613f0cfd2dfa87394dbc Mon Sep 17 00:00:00 2001 From: Ashley Shenton Date: Mon, 4 Dec 2023 15:34:47 +0000 Subject: [PATCH 015/110] update incorrect DocComment for isOnlyBuckets method (#132) --- src/Entry.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Entry.php b/src/Entry.php index bc753fc4..5b3e6647 100644 --- a/src/Entry.php +++ b/src/Entry.php @@ -93,7 +93,7 @@ public function isAvg(): bool } /** - * Determine whether the entry is marked for average aggregation. + * Determine whether to only save aggregate bucket data for the entry. */ public function isOnlyBuckets(): bool { From fb370f284037c3befb5a67dfee317c4e442b5a72 Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Tue, 5 Dec 2023 03:11:17 +1100 Subject: [PATCH 016/110] [1.x] Improve redis adapter (#127) * Remove unused redis adapter methods * Fix code styling * Further improvements * Fix code styling --------- Co-authored-by: timacdonald --- composer.json | 2 +- src/Support/RedisAdapter.php | 159 +++++++++------------ src/Support/RedisClientException.php | 10 ++ tests/Feature/RedisTest.php | 201 +++++++++------------------ 4 files changed, 139 insertions(+), 233 deletions(-) create mode 100644 src/Support/RedisClientException.php diff --git a/composer.json b/composer.json index 68115604..0cfeb930 100644 --- a/composer.json +++ b/composer.json @@ -41,7 +41,7 @@ "pestphp/pest": "^2.0", "pestphp/pest-plugin-laravel": "^2.2", "phpstan/phpstan": "^1.11", - "predis/predis": "^2.2" + "predis/predis": "^1.0 || ^2.0" }, "conflict": { "nunomaduro/collision": "<7.7.0" diff --git a/src/Support/RedisAdapter.php b/src/Support/RedisAdapter.php index 24283198..301f4084 100644 --- a/src/Support/RedisAdapter.php +++ b/src/Support/RedisAdapter.php @@ -2,14 +2,14 @@ namespace Laravel\Pulse\Support; -use Carbon\CarbonInterval; use Illuminate\Config\Repository; use Illuminate\Redis\Connections\Connection; use Illuminate\Support\Collection; use Predis\Client as Predis; +use Predis\Command\RawCommand; use Predis\Pipeline\Pipeline; +use Predis\Response\ServerException as PredisServerException; use Redis as PhpRedis; -use RuntimeException; /** * @internal @@ -27,84 +27,6 @@ public function __construct( // } - /** - * Get a range from a sorted set. - * - * @return list - */ - public function zrange(string $key, int $start, int $stop, bool $reversed = false, bool $withScores = false): array|PhpRedis|Pipeline - { - return match (true) { - $this->client() instanceof PhpRedis => $this->client()->rawCommand('ZRANGE', $this->config->get('database.redis.options.prefix').$key, $start, $stop, ...array_filter([ - $reversed ? 'REV' : null, - $withScores ? 'WITHSCORES' : null, - ])), - $this->client() instanceof Predis || - $this->client() instanceof Pipeline => $this->client()->zrange($key, $start, $stop, ...array_filter([ - $reversed ? 'REV' : null, - $withScores ? 'WITHSCORES' : null, - ])), - }; - } - - /** - * Remove sorted set range by score. - */ - public function zremrangebyscore(string $key, int|string $start, int|string $stop): int|PhpRedis|Pipeline - { - return match (true) { - $this->client() instanceof PhpRedis => $this->client()->rawCommand('ZREMRANGEBYSCORE', $this->config->get('database.redis.options.prefix').$key, $start, $stop), - $this->client() instanceof Predis || - $this->client() instanceof Pipeline => $this->client()->zremrangebyscore($key, $start, $stop), - }; - } - - /** - * Get the key. - */ - public function get(string $key): null|string|Pipeline|PhpRedis - { - return $this->client()->get($key); // @phpstan-ignore return.type - } - - /** - * Set the value. - */ - public function set(string $key, string $value, CarbonInterval $ttl): null|string|Pipeline|PhpRedis - { - return match (true) { - $this->client() instanceof PhpRedis => $this->client()->rawCommand('SET', $this->config->get('database.redis.options.prefix').$key, $value, 'PX', (int) $ttl->totalMilliseconds), - $this->client() instanceof Predis || - $this->client() instanceof Pipeline => $this->client()->set($key, $value, 'PX', (int) $ttl->totalMilliseconds), - }; - } - - /** - * Expire the key at the given time. - */ - public function expire(string $key, CarbonInterval $interval): int|Pipeline|PhpRedis - { - return $this->client()->expire($key, (int) $interval->totalSeconds); // @phpstan-ignore return.type - } - - /** - * Delete the key. - * - * @param list $keys - */ - public function del(array $keys): int|Pipeline|PhpRedis - { - return $this->client()->del(...$keys); // @phpstan-ignore return.type - } - - /** - * Increment the given members value. - */ - public function zincrby(string $key, int $increment, string $member): string|float|Pipeline|PhpRedis - { - return $this->client()->zincrby($key, $increment, $member); // @phpstan-ignore return.type - } - /** * Add an entry to the stream. * @@ -112,11 +34,12 @@ public function zincrby(string $key, int $increment, string $member): string|flo */ public function xadd(string $key, array $dictionary): string|Pipeline|PhpRedis { - return match (true) { - $this->client() instanceof PhpRedis => $this->client()->xadd($key, '*', $dictionary), - $this->client() instanceof Predis || - $this->client() instanceof Pipeline => $this->client()->xadd($key, $dictionary), // @phpstan-ignore method.notFound - }; + return $this->handle([ + 'XADD', + $this->config->get('database.redis.options.prefix').$key, + '*', + ...collect($dictionary)->keys()->zip($dictionary)->flatten()->all(), + ]); } /** @@ -126,7 +49,19 @@ public function xadd(string $key, array $dictionary): string|Pipeline|PhpRedis */ public function xrange(string $key, string $start, string $end, int $count = null): array { - return $this->client()->xrange(...array_filter(func_get_args())); // @phpstan-ignore method.notFound + return collect($this->handle([ + 'XRANGE', + $this->config->get('database.redis.options.prefix').$key, + $start, + $end, + ...$count !== null ? ['COUNT', "$count"] : [], + ]))->mapWithKeys(fn ($value, $key) => [ + $value[0] => collect($value[1]) + ->chunk(2) + ->map->values() + ->mapWithKeys(fn ($value, $key) => [$value[0] => $value[1]]) + ->all(), + ])->all(); } /** @@ -134,11 +69,13 @@ public function xrange(string $key, string $start, string $end, int $count = nul */ public function xtrim(string $key, string $strategy, string $strategyModifier, string|int $threshold): int { - return match (true) { - $this->client() instanceof PhpRedis => $this->client()->rawCommand('XTRIM', $this->config->get('database.redis.options.prefix').$key, $strategy, $strategyModifier, (string) $threshold), - $this->client() instanceof Predis || - $this->client() instanceof Pipeline => $this->client()->xtrim($key, [$strategy, $strategyModifier], (string) $threshold), // @phpstan-ignore method.notFound - }; + return $this->handle([ + 'XTRIM', + $this->config->get('database.redis.options.prefix').$key, + $strategy, + $strategyModifier, + $threshold, + ]); } /** @@ -148,7 +85,11 @@ public function xtrim(string $key, string $strategy, string $strategyModifier, s */ public function xdel(string $stream, Collection|array $keys): int { - return $this->client()->xdel($stream, Collection::unwrap($keys)); // @phpstan-ignore method.notFound + return $this->handle([ + 'XDEL', + $this->config->get('database.redis.options.prefix').$stream, + ...$keys, + ]); } /** @@ -159,16 +100,40 @@ public function xdel(string $stream, Collection|array $keys): int */ public function pipeline(callable $closure): array { - if ($this->client() instanceof Pipeline) { - throw new RuntimeException('Pipelines are not able to be nested.'); - } - // Create a pipeline and wrap the Redis client in an instance of this class to ensure our wrapper methods are used within the pipeline... return $this->connection->pipeline(fn (Pipeline|PhpRedis $client) => $closure(new self($this->connection, $this->config, $client))); // @phpstan-ignore method.notFound } /** - * Get the Redis client instance. + * Run the given command. + */ + protected function handle($args): mixed + { + try { + return tap($this->run($args), function ($result) { + if ($result === false && $this->client() instanceof PhpRedis) { + throw new RedisClientException($this->client()->getLastError()); + } + }); + } catch (PredisServerException $e) { + throw new RedisClientException($e->getMessage(), previous: $e); + } + } + + /** + * Run the given command. + */ + protected function run($args): mixed + { + return match (true) { + $this->client() instanceof PhpRedis => $this->client()->rawCommand(...$args), + $this->client() instanceof Predis, + $this->client() instanceof Pipeline => $this->client()->executeCommand(new RawCommand($args)), + }; + } + + /** + * Retrieve the Redis client. */ protected function client(): PhpRedis|Predis|Pipeline { diff --git a/src/Support/RedisClientException.php b/src/Support/RedisClientException.php new file mode 100644 index 00000000..84ac27e4 --- /dev/null +++ b/src/Support/RedisClientException.php @@ -0,0 +1,10 @@ + Process::timeout(1)->run('redis-cli -p '.Config::get('database.redis.default.port').' FLUSHALL')->throw()); @@ -48,155 +49,85 @@ ->output(); [$firstEntryKey, $lastEntryKey] = collect(explode("\n", $output))->only([17, 21])->values(); - $commands = captureRedisCommands(fn () => $ingest->store(new NullStorage)); + $commands = captureRedisCommands(fn () => $ingest->store(new StorageFake())); expect($commands)->toContain('"XRANGE" "laravel_database_laravel:pulse:ingest" "-" "+" "COUNT" "567"'); expect($commands)->toContain('"XDEL" "laravel_database_laravel:pulse:ingest" "'.$firstEntryKey.'" "'.$lastEntryKey.'"'); })->with(['predis', 'phpredis']); -it('runs the same zincrby command', function ($driver) { +it('has consistent return for xadd', function ($driver) { Config::set('database.redis.client', $driver); - $redis = new RedisAdapter(FacadesRedis::connection(), App::make('config')); - - $commands = captureRedisCommands(fn () => $redis->zincrby('MYKEY', '55', 'my-member')); - expect($commands)->toContain('"ZINCRBY" "laravel_database_MYKEY" "55" "my-member"'); - - $commands = captureRedisCommands(fn () => $redis->zincrby('MYKEY', '-55', 'my-member')); - expect($commands)->toContain('"ZINCRBY" "laravel_database_MYKEY" "-55" "my-member"'); - - $commands = captureRedisCommands(fn () => $redis->pipeline(fn ($r) => $r->zincrby('MYKEY', '-55', 'my-member'))); - expect($commands)->toContain('"ZINCRBY" "laravel_database_MYKEY" "-55" "my-member"'); -})->with(['predis', 'phpredis']); - -it('runs the same zrange command', function ($driver) { - Config::set('database.redis.client', $driver); - $redis = new RedisAdapter(FacadesRedis::connection(), App::make('config')); - - $commands = captureRedisCommands(fn () => $redis->zrange('MYKEY', 2, 3, reversed: false, withScores: false)); - expect($commands)->toContain('"ZRANGE" "laravel_database_MYKEY" "2" "3"'); - - $commands = captureRedisCommands(fn () => $redis->zrange('MYKEY', 2, 3, reversed: true, withScores: false)); - expect($commands)->toContain('"ZRANGE" "laravel_database_MYKEY" "2" "3" "REV"'); - - $commands = captureRedisCommands(fn () => $redis->zrange('MYKEY', 2, 3, reversed: false, withScores: true)); - expect($commands)->toContain('"ZRANGE" "laravel_database_MYKEY" "2" "3" "WITHSCORES"'); - - $commands = captureRedisCommands(fn () => $redis->zrange('MYKEY', 2, 3, reversed: true, withScores: true)); - expect($commands)->toContain('"ZRANGE" "laravel_database_MYKEY" "2" "3" "REV" "WITHSCORES"'); - - $commands = captureRedisCommands(fn () => $redis->pipeline(fn ($r) => $r->zrange('MYKEY', 2, 3, reversed: true, withScores: true))); - expect($commands)->toContain('"ZRANGE" "laravel_database_MYKEY" "2" "3" "REV" "WITHSCORES"'); + $redis = new RedisAdapter(Redis::connection(), App::make('config')); + + $result = $redis->xadd('stream-name', [ + 'foo' => 1, + 'bar' => 2, + ]); + + expect($result)->toBeString(); + $parts = explode('-', $result); + expect($parts)->toHaveCount(2); + expect($parts[0])->toEqualWithDelta(now()->getTimestampMs(), 50); + expect($parts[1])->toBe('0'); })->with(['predis', 'phpredis']); -it('runs the same get command', function ($driver) { +it('has consistent return for xrange', function ($driver) { Config::set('database.redis.client', $driver); - $redis = new RedisAdapter(FacadesRedis::connection(), App::make('config')); - - $commands = captureRedisCommands(fn () => $redis->get('MYKEY')); - expect($commands)->toContain('"GET" "laravel_database_MYKEY"'); - - $commands = captureRedisCommands(fn () => $redis->pipeline(fn ($r) => $redis->get('MYKEY'))); - expect($commands)->toContain('"GET" "laravel_database_MYKEY"'); + $redis = new RedisAdapter(Redis::connection(), App::make('config')); + $redis->xadd('stream-name', [ + 'foo' => 1, + 'bar' => 2, + ]); + $redis->xadd('stream-name', [ + 'foo' => 3, + 'bar' => 4, + ]); + + $result = $redis->xrange('stream-name', '-', '+', 1000); + + expect($result)->toBeArray(); + expect($result)->toHaveCount(2); + $values = [ + ['foo' => '1', 'bar' => '2'], + ['foo' => '3', 'bar' => '4'], + ]; + foreach ($result as $key => $value) { + $parts = explode('-', $key); + expect($parts)->toHaveCount(2); + expect($parts[0])->toEqualWithDelta(now()->getTimestampMs(), 50); + expect($parts[1])->toBeIn(['0', '1']); + expect($value)->toBe(array_shift($values)); + } })->with(['predis', 'phpredis']); -it('runs the same set command', function ($driver) { +it('has consistent return for xtrim', function ($driver) { Config::set('database.redis.client', $driver); - $redis = new RedisAdapter(FacadesRedis::connection(), App::make('config')); + $redis = new RedisAdapter(Redis::connection(), App::make('config')); - $commands = captureRedisCommands(fn () => $redis->set('MYKEY', 'myvalue', CarbonInterval::seconds(5))); - expect($commands)->toContain('"SET" "laravel_database_MYKEY" "myvalue" "PX" "5000"'); + $redis->xadd('stream-name', [ + 'foo' => 1, + 'bar' => 2, + ]); + $redis->xadd('stream-name', [ + 'foo' => 3, + 'bar' => 4, + ]); - $commands = captureRedisCommands(fn () => $redis->pipeline(fn ($r) => $r->set('MYKEY', 'myvalue', CarbonInterval::seconds(5)))); - expect($commands)->toContain('"SET" "laravel_database_MYKEY" "myvalue" "PX" "5000"'); -})->with(['predis', 'phpredis']); + Sleep::for(5)->milliseconds(); -it('runs the same del command', function ($driver) { - Config::set('database.redis.client', $driver); - $redis = new RedisAdapter(FacadesRedis::connection(), App::make('config')); + $lastKey = $redis->xadd('stream-name', [ + 'foo' => 5, + 'bar' => 6, + ]); - $commands = captureRedisCommands(fn () => $redis->del(['MYKEY', 'MYOTHERKEY'])); - expect($commands)->toContain('"DEL" "laravel_database_MYKEY" "laravel_database_MYOTHERKEY"'); + $result = $redis->xtrim('stream-name', 'MINID', '=', Str::before($lastKey, '-')); - $commands = captureRedisCommands(fn () => $redis->pipeline(fn ($r) => $r->del(['MYKEY', 'MYOTHERKEY']))); - expect($commands)->toContain('"DEL" "laravel_database_MYKEY" "laravel_database_MYOTHERKEY"'); + expect($result)->toBe(2); })->with(['predis', 'phpredis']); -it('runs the same expire command', function ($driver) { +it('throws exception on failure', function ($driver) { Config::set('database.redis.client', $driver); - $redis = new RedisAdapter(FacadesRedis::connection(), App::make('config')); - - $commands = captureRedisCommands(fn () => $redis->expire('MYKEY', CarbonInterval::day())); - expect($commands)->toContain('"EXPIRE" "laravel_database_MYKEY" "86400"'); + $redis = new RedisAdapter(Redis::connection(), App::make('config')); - $commands = captureRedisCommands(fn () => $redis->pipeline(fn ($r) => $r->expire('MYKEY', CarbonInterval::day()))); - expect($commands)->toContain('"EXPIRE" "laravel_database_MYKEY" "86400"'); -})->with(['predis', 'phpredis']); - -it('runs the same remrangebyscore command', function ($driver) { - Config::set('database.redis.client', $driver); - $redis = new RedisAdapter(FacadesRedis::connection(), App::make('config')); - - $commands = captureRedisCommands(fn () => $redis->zremrangebyscore('MYKEY', 0, 10)); - expect($commands)->toContain('"ZREMRANGEBYSCORE" "laravel_database_MYKEY" "0" "10"'); - - $commands = captureRedisCommands(fn () => $redis->pipeline(fn ($r) => $r->zremrangebyscore('MYKEY', 0, 10))); - expect($commands)->toContain('"ZREMRANGEBYSCORE" "laravel_database_MYKEY" "0" "10"'); -})->with(['predis', 'phpredis']); - -class NullStorage implements Storage -{ - public function store(Collection $items): void - { - // - } - - public function trim(): void - { - // - } - - public function purge(array $types = null): void - { - // - } - - public function values(string $type, array $keys = null): Collection - { - return collect(); - } - - public function graph(array $types, string $aggregate, CarbonInterval $interval): Collection - { - return collect(); - } - - public function aggregate( - string $type, - array|string $aggregates, - CarbonInterval $interval, - string $orderBy = null, - string $direction = 'desc', - int $limit = 101, - ): Collection { - return collect(); - } - - public function aggregateTypes( - string|array $types, - string $aggregate, - CarbonInterval $interval, - string $orderBy = null, - string $direction = 'desc', - int $limit = 101, - ): Collection { - return collect(); - } - - public function aggregateTotal( - array|string $types, - string $aggregate, - CarbonInterval $interval, - ): Collection { - return collect(); - } -} + $redis->xtrim('stream-name', 'FOO', 'a', 'xyz'); +})->with(['predis', 'phpredis'])->throws(RedisClientException::class, 'ERR syntax error'); From ae0af990b5676b971bf7989cef0772f615933644 Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Tue, 5 Dec 2023 03:13:03 +1100 Subject: [PATCH 017/110] [1.x] Improve test configuration and bump min dependencies (#126) * Test against min stability * Test against min PHP version * remove already required dependencies * Bump min requirements --- .github/workflows/tests.yml | 8 ++++---- composer.json | 28 ++++++++++++++-------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9b094c48..c548c8b4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -31,10 +31,11 @@ jobs: strategy: fail-fast: true matrix: - php: [8.2, 8.3] + php: [8.1, 8.2, 8.3] laravel: [10] + stability: [prefer-lowest, prefer-stable] - name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} + name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} - Stability {{ matrix.stability }} steps: - name: Checkout code @@ -54,8 +55,7 @@ jobs: - name: Install dependencies run: | - composer require "illuminate/contracts=^${{ matrix.laravel }}" --dev --no-update - composer update --prefer-dist --no-interaction --no-progress + composer update --prefer-dist --no-interaction --no-progress --${{ matrix.stability }} - name: Execute tests run: vendor/bin/pest diff --git a/composer.json b/composer.json index 0cfeb930..33be9f82 100644 --- a/composer.json +++ b/composer.json @@ -18,20 +18,20 @@ "php": "^8.1", "doctrine/sql-formatter": "^1.1", "guzzlehttp/promises": "^1.0 || ^2.0", - "illuminate/auth": "^10.20", - "illuminate/cache": "^10.20", - "illuminate/config": "^10.20", - "illuminate/console": "^10.20", - "illuminate/contracts": "^10.20", - "illuminate/database": "^10.20", - "illuminate/events": "^10.20", - "illuminate/http": "^10.20", - "illuminate/queue": "^10.20", - "illuminate/redis": "^10.20", - "illuminate/routing": "^10.20", - "illuminate/support": "^10.20", - "illuminate/view": "^10.20", - "livewire/livewire": "^3.0", + "illuminate/auth": "^10.21", + "illuminate/cache": "^10.21", + "illuminate/config": "^10.21", + "illuminate/console": "^10.21", + "illuminate/contracts": "^10.21", + "illuminate/database": "^10.21", + "illuminate/events": "^10.21", + "illuminate/http": "^10.21", + "illuminate/queue": "^10.21", + "illuminate/redis": "^10.21", + "illuminate/routing": "^10.21", + "illuminate/support": "^10.21", + "illuminate/view": "^10.21", + "livewire/livewire": "^3.02", "nesbot/carbon": "^2.67" }, "require-dev": { From 56014a8c1a47c8552e5140829f20eaf109f9e473 Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Tue, 5 Dec 2023 03:14:01 +1100 Subject: [PATCH 018/110] Remove guard as min framework version has been increased (#123) --- .../livewire/slow-outgoing-requests.blade.php | 114 ++++++++---------- src/Livewire/SlowOutgoingRequests.php | 2 - src/Recorders/SlowOutgoingRequests.php | 4 +- .../Livewire/SlowOutgoingRequestsTest.php | 3 +- 4 files changed, 55 insertions(+), 68 deletions(-) diff --git a/resources/views/livewire/slow-outgoing-requests.blade.php b/resources/views/livewire/slow-outgoing-requests.blade.php index 18979530..9fde1ddb 100644 --- a/resources/views/livewire/slow-outgoing-requests.blade.php +++ b/resources/views/livewire/slow-outgoing-requests.blade.php @@ -37,70 +37,62 @@
- @if (! $supported) -
-
- Requires laravel/framework v10.14+ -
-
+ @if ($slowOutgoingRequests->isEmpty()) + @else - @if ($slowOutgoingRequests->isEmpty()) - - @else - - - - - - - - - - Method - URI - Count - Slowest - - - - @foreach ($slowOutgoingRequests->take(100) as $request) - - - - - - -
- @if ($host = parse_url($request->uri, PHP_URL_HOST)) - - @endif - - {{ $request->uri }} - -
-
- - @if ($config['sample_rate'] < 1) - ~{{ number_format($request->count * (1 / $config['sample_rate'])) }} - @else - {{ number_format($request->count) }} - @endif - - - @if ($request->slowest === null) - Unknown - @else - {{ number_format($request->slowest) ?: '<1' }} ms + + + + + + + + + + Method + URI + Count + Slowest + + + + @foreach ($slowOutgoingRequests->take(100) as $request) + + + + + + +
+ @if ($host = parse_url($request->uri, PHP_URL_HOST)) + @endif - - - @endforeach - - + + {{ $request->uri }} + +
+
+ + @if ($config['sample_rate'] < 1) + ~{{ number_format($request->count * (1 / $config['sample_rate'])) }} + @else + {{ number_format($request->count) }} + @endif + + + @if ($request->slowest === null) + Unknown + @else + {{ number_format($request->slowest) ?: '<1' }} ms + @endif + + + @endforeach + +
- @if ($slowOutgoingRequests->count() > 100) -
Limited to 100 entries
- @endif + @if ($slowOutgoingRequests->count() > 100) +
Limited to 100 entries
@endif @endif
diff --git a/src/Livewire/SlowOutgoingRequests.php b/src/Livewire/SlowOutgoingRequests.php index 903e935a..dfcec094 100644 --- a/src/Livewire/SlowOutgoingRequests.php +++ b/src/Livewire/SlowOutgoingRequests.php @@ -3,7 +3,6 @@ namespace Laravel\Pulse\Livewire; use Illuminate\Contracts\Support\Renderable; -use Illuminate\Http\Client\Factory; use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\View; use Laravel\Pulse\Facades\Pulse; @@ -59,7 +58,6 @@ public function render(): Renderable 'runAt' => $runAt, 'config' => Config::get('pulse.recorders.'.SlowOutgoingRequestsRecorder::class), 'slowOutgoingRequests' => $slowOutgoingRequests, - 'supported' => method_exists(Factory::class, 'globalMiddleware'), ]); } } diff --git a/src/Recorders/SlowOutgoingRequests.php b/src/Recorders/SlowOutgoingRequests.php index b52401f7..10d27869 100644 --- a/src/Recorders/SlowOutgoingRequests.php +++ b/src/Recorders/SlowOutgoingRequests.php @@ -39,9 +39,7 @@ public function __construct( */ public function register(callable $record, Application $app): void { - if (method_exists(Factory::class, 'globalMiddleware')) { - $this->afterResolving($app, Factory::class, fn (Factory $factory) => $factory->globalMiddleware($this->middleware($record))); - } + $this->afterResolving($app, Factory::class, fn (Factory $factory) => $factory->globalMiddleware($this->middleware($record))); } /** diff --git a/tests/Feature/Livewire/SlowOutgoingRequestsTest.php b/tests/Feature/Livewire/SlowOutgoingRequestsTest.php index ae030025..cfc7fa7a 100644 --- a/tests/Feature/Livewire/SlowOutgoingRequestsTest.php +++ b/tests/Feature/Livewire/SlowOutgoingRequestsTest.php @@ -35,6 +35,5 @@ ->assertViewHas('slowOutgoingRequests', collect([ (object) ['method' => 'GET', 'uri' => 'http://example.com', 'count' => 4, 'slowest' => 2468], (object) ['method' => 'GET', 'uri' => 'http://example.org', 'count' => 2, 'slowest' => 1234], - ])) - ->assertViewHas('supported', true); + ])); }); From 9ab920b5894582698669cc9c0b7e8b3aa74465d5 Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Tue, 5 Dec 2023 03:14:54 +1100 Subject: [PATCH 019/110] Ensure catch all routes do not take precedence (#124) --- src/PulseServiceProvider.php | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/PulseServiceProvider.php b/src/PulseServiceProvider.php index 8adf25b2..7d31aea4 100644 --- a/src/PulseServiceProvider.php +++ b/src/PulseServiceProvider.php @@ -95,17 +95,15 @@ protected function registerAuthorization(): void */ protected function registerRoutes(): void { - $this->app->booted(function () { - $this->callAfterResolving('router', function (Router $router, Application $app) { - $router->group([ - 'domain' => $app->make('config')->get('pulse.domain', null), - 'prefix' => $app->make('config')->get('pulse.path'), - 'middleware' => $app->make('config')->get('pulse.middleware', 'web'), - ], function (Router $router) { - $router->get('/', function (Pulse $pulse, ViewFactory $view) { - return $view->make('pulse::dashboard'); - })->name('pulse'); - }); + $this->callAfterResolving('router', function (Router $router, Application $app) { + $router->group([ + 'domain' => $app->make('config')->get('pulse.domain', null), + 'prefix' => $app->make('config')->get('pulse.path'), + 'middleware' => $app->make('config')->get('pulse.middleware', 'web'), + ], function (Router $router) { + $router->get('/', function (Pulse $pulse, ViewFactory $view) { + return $view->make('pulse::dashboard'); + })->name('pulse'); }); }); } From 6d27eede84bed499b5baaf720a14ccec7f6bb9c9 Mon Sep 17 00:00:00 2001 From: Dan Wall Date: Tue, 5 Dec 2023 02:15:29 +1000 Subject: [PATCH 020/110] [1.x] Allow concern usage for 3rd parties (#118) * Allow recorder inheritance * Recorder inheritance - only update Concerns --- src/Recorders/Concerns/Groups.php | 2 +- src/Recorders/Concerns/Ignores.php | 2 +- src/Recorders/Concerns/Sampling.php | 4 ++-- src/Recorders/Concerns/Thresholds.php | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Recorders/Concerns/Groups.php b/src/Recorders/Concerns/Groups.php index efc57fe3..b937ccb7 100644 --- a/src/Recorders/Concerns/Groups.php +++ b/src/Recorders/Concerns/Groups.php @@ -9,7 +9,7 @@ trait Groups */ protected function group(string $value): string { - foreach ($this->config->get('pulse.recorders.'.self::class.'.groups') as $pattern => $replacement) { + foreach ($this->config->get('pulse.recorders.'.static::class.'.groups') as $pattern => $replacement) { $group = preg_replace($pattern, $replacement, $value, count: $count); if ($count > 0 && $group !== null) { diff --git a/src/Recorders/Concerns/Ignores.php b/src/Recorders/Concerns/Ignores.php index d7d0bf9a..3edafedc 100644 --- a/src/Recorders/Concerns/Ignores.php +++ b/src/Recorders/Concerns/Ignores.php @@ -10,7 +10,7 @@ trait Ignores protected function shouldIgnore(string $value): bool { // @phpstan-ignore argument.templateType, argument.templateType - return collect($this->config->get('pulse.recorders.'.self::class.'.ignore')) + return collect($this->config->get('pulse.recorders.'.static::class.'.ignore')) ->contains(fn (string $pattern) => preg_match($pattern, $value)); } } diff --git a/src/Recorders/Concerns/Sampling.php b/src/Recorders/Concerns/Sampling.php index 29233d0f..d68df8aa 100644 --- a/src/Recorders/Concerns/Sampling.php +++ b/src/Recorders/Concerns/Sampling.php @@ -12,7 +12,7 @@ trait Sampling protected function shouldSample(): bool { return Lottery::odds( - $this->config->get('pulse.recorders.'.self::class.'.sample_rate') + $this->config->get('pulse.recorders.'.static::class.'.sample_rate') )->choose(); } @@ -23,6 +23,6 @@ protected function shouldSampleDeterministically(string $seed): bool { $value = hexdec(md5($seed)) / pow(16, 32); // Scale to 0-1 - return $value <= $this->config->get('pulse.recorders.'.self::class.'.sample_rate'); + return $value <= $this->config->get('pulse.recorders.'.static::class.'.sample_rate'); } } diff --git a/src/Recorders/Concerns/Thresholds.php b/src/Recorders/Concerns/Thresholds.php index c182ee29..4ef2efaa 100644 --- a/src/Recorders/Concerns/Thresholds.php +++ b/src/Recorders/Concerns/Thresholds.php @@ -9,6 +9,6 @@ trait Thresholds */ protected function underThreshold(int|float $duration): bool { - return $duration < $this->config->get('pulse.recorders.'.self::class.'.threshold'); + return $duration < $this->config->get('pulse.recorders.'.static::class.'.threshold'); } } From 37f64394258d7e690667010cc587c4ec1bc95a83 Mon Sep 17 00:00:00 2001 From: Pierre Pavlov Date: Mon, 4 Dec 2023 17:58:55 +0100 Subject: [PATCH 021/110] [1.x] Add possibility to change cache duration (#113) * Add possibility to change cache duration * Support wider types to match underlying remember types --------- Co-authored-by: Tim MacDonald --- src/Livewire/Concerns/RemembersQueries.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Livewire/Concerns/RemembersQueries.php b/src/Livewire/Concerns/RemembersQueries.php index cf3707e5..ef2d664e 100644 --- a/src/Livewire/Concerns/RemembersQueries.php +++ b/src/Livewire/Concerns/RemembersQueries.php @@ -4,6 +4,9 @@ use Carbon\CarbonImmutable; use Carbon\CarbonInterval; +use Closure; +use DateInterval; +use DateTimeInterface; use Illuminate\Support\Benchmark; use Illuminate\Support\Facades\App; use Laravel\Pulse\Support\CacheStoreResolver; @@ -15,9 +18,9 @@ trait RemembersQueries * * @return array{0: mixed, 1: float, 2: string} */ - public function remember(callable $query, string $key = ''): array + public function remember(callable $query, string $key = '', DateTimeInterface|DateInterval|Closure|int|null $ttl = 5): array { - return App::make(CacheStoreResolver::class)->store()->remember('laravel:pulse:'.static::class.':'.$key.':'.$this->period, CarbonInterval::seconds(5), function () use ($query) { + return App::make(CacheStoreResolver::class)->store()->remember('laravel:pulse:'.static::class.':'.$key.':'.$this->period, $ttl, function () use ($query) { $start = CarbonImmutable::now()->toDateTimeString(); [$value, $duration] = Benchmark::value(fn () => $query($this->periodAsInterval())); From 4318f01e39eea3dea6c0ad3412ffb58511384905 Mon Sep 17 00:00:00 2001 From: taylorotwell Date: Mon, 4 Dec 2023 16:59:20 +0000 Subject: [PATCH 022/110] Fix code styling --- src/Livewire/Concerns/RemembersQueries.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Livewire/Concerns/RemembersQueries.php b/src/Livewire/Concerns/RemembersQueries.php index ef2d664e..90a2e7bb 100644 --- a/src/Livewire/Concerns/RemembersQueries.php +++ b/src/Livewire/Concerns/RemembersQueries.php @@ -3,7 +3,6 @@ namespace Laravel\Pulse\Livewire\Concerns; use Carbon\CarbonImmutable; -use Carbon\CarbonInterval; use Closure; use DateInterval; use DateTimeInterface; From d9bc892ea126edd066284f122c993c7a153a3ba3 Mon Sep 17 00:00:00 2001 From: Punyapal Shah <53343069+mr-punyapal@users.noreply.github.com> Date: Mon, 4 Dec 2023 22:36:03 +0530 Subject: [PATCH 023/110] [1.x] Fix file path in SlowQueries.php AND Exceptions.php (#104) * Fix file path in SlowQueries.php * Fix base path separator in formatLocation method --- src/Recorders/Exceptions.php | 2 +- src/Recorders/SlowQueries.php | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Recorders/Exceptions.php b/src/Recorders/Exceptions.php index a278a8d9..7a7e9102 100644 --- a/src/Recorders/Exceptions.php +++ b/src/Recorders/Exceptions.php @@ -136,6 +136,6 @@ protected function isInternalFile(string $file): bool */ protected function formatLocation(string $file, ?int $line): string { - return Str::replaceFirst(base_path('/'), '', $file).(is_int($line) ? (':'.$line) : ''); + return Str::replaceFirst(base_path(DIRECTORY_SEPARATOR), '', $file).(is_int($line) ? (':'.$line) : ''); } } diff --git a/src/Recorders/SlowQueries.php b/src/Recorders/SlowQueries.php index 9da1bfd4..a1e74ef0 100644 --- a/src/Recorders/SlowQueries.php +++ b/src/Recorders/SlowQueries.php @@ -85,8 +85,8 @@ protected function resolveLocation(): string */ protected function isInternalFile(string $file): bool { - return Str::startsWith($file, base_path('vendor/laravel/pulse')) - || Str::startsWith($file, base_path('vendor/laravel/framework')) + return Str::startsWith($file, base_path('vendor'.DIRECTORY_SEPARATOR.'laravel'.DIRECTORY_SEPARATOR.'pulse')) + || Str::startsWith($file, base_path('vendor'.DIRECTORY_SEPARATOR.'laravel'.DIRECTORY_SEPARATOR.'framework')) || $file === base_path('artisan') || $file === public_path('index.php'); } @@ -96,6 +96,6 @@ protected function isInternalFile(string $file): bool */ protected function formatLocation(string $file, ?int $line): string { - return Str::replaceFirst(base_path('/'), '', $file).(is_int($line) ? (':'.$line) : ''); + return Str::replaceFirst(base_path(DIRECTORY_SEPARATOR), '', $file).(is_int($line) ? (':'.$line) : ''); } } From 25b48add86ca752c94ff7031cbedb982638ce430 Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Tue, 5 Dec 2023 15:24:46 +1100 Subject: [PATCH 024/110] Fix stability name --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c548c8b4..080571cd 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -35,7 +35,7 @@ jobs: laravel: [10] stability: [prefer-lowest, prefer-stable] - name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} - Stability {{ matrix.stability }} + name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} - Stability ${{ matrix.stability }} steps: - name: Checkout code From 03849dcdcceee8e0f33df77f35a4c8236744495e Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Tue, 5 Dec 2023 15:33:39 +1100 Subject: [PATCH 025/110] Redis adapter improvements --- src/Support/RedisAdapter.php | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/Support/RedisAdapter.php b/src/Support/RedisAdapter.php index 301f4084..9a773238 100644 --- a/src/Support/RedisAdapter.php +++ b/src/Support/RedisAdapter.php @@ -49,14 +49,14 @@ public function xadd(string $key, array $dictionary): string|Pipeline|PhpRedis */ public function xrange(string $key, string $start, string $end, int $count = null): array { - return collect($this->handle([ + return collect($this->handle([ // @phpstan-ignore return.type argument.templateType argument.templateType 'XRANGE', $this->config->get('database.redis.options.prefix').$key, $start, $end, ...$count !== null ? ['COUNT', "$count"] : [], ]))->mapWithKeys(fn ($value, $key) => [ - $value[0] => collect($value[1]) + $value[0] => collect($value[1]) // @phpstan-ignore argument.templateType argument.templateType ->chunk(2) ->map->values() ->mapWithKeys(fn ($value, $key) => [$value[0] => $value[1]]) @@ -74,7 +74,7 @@ public function xtrim(string $key, string $strategy, string $strategyModifier, s $this->config->get('database.redis.options.prefix').$key, $strategy, $strategyModifier, - $threshold, + (string) $threshold, ]); } @@ -106,13 +106,15 @@ public function pipeline(callable $closure): array /** * Run the given command. + * + * @param list $args */ - protected function handle($args): mixed + protected function handle(array $args): mixed { try { return tap($this->run($args), function ($result) { if ($result === false && $this->client() instanceof PhpRedis) { - throw new RedisClientException($this->client()->getLastError()); + throw new RedisClientException($this->client()->getLastError() ?? 'An unknown error occurred.'); } }); } catch (PredisServerException $e) { @@ -122,13 +124,15 @@ protected function handle($args): mixed /** * Run the given command. + * + * @param list $args */ - protected function run($args): mixed + protected function run(array $args): mixed { return match (true) { $this->client() instanceof PhpRedis => $this->client()->rawCommand(...$args), $this->client() instanceof Predis, - $this->client() instanceof Pipeline => $this->client()->executeCommand(new RawCommand($args)), + $this->client() instanceof Pipeline => $this->client()->executeCommand(RawCommand::create(...$args)), }; } From 03e1c2262213b5710ee09c848190bea4b7924cfe Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Wed, 6 Dec 2023 01:45:06 +1100 Subject: [PATCH 026/110] [1.x] Support DB configurations that _require_ a unique ID (#142) * Support DB configurations that _require_ a unique ID * Add upgrading guide * Unset the ID for expectations * Fix code styling * Update UPGRADE.MD --------- Co-authored-by: timacdonald Co-authored-by: James Brooks --- UPGRADE.MD | 6 ++++++ .../migrations/2023_06_07_000001_create_pulse_tables.php | 3 +++ tests/Pest.php | 4 ++++ 3 files changed, 13 insertions(+) create mode 100644 UPGRADE.MD diff --git a/UPGRADE.MD b/UPGRADE.MD new file mode 100644 index 00000000..2934af35 --- /dev/null +++ b/UPGRADE.MD @@ -0,0 +1,6 @@ +# Upgrade Guide + +# Beta to 1.x + +- [Auto-incrementing IDs were added to Pulse's tables](https://github.com/laravel/pulse/pull/142). This is recommended if you are using a configuration that requires tables to have a unique key on every table, e.g., PlanetScale. + 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 39141738..ee244372 100644 --- a/database/migrations/2023_06_07_000001_create_pulse_tables.php +++ b/database/migrations/2023_06_07_000001_create_pulse_tables.php @@ -22,6 +22,7 @@ public function up(): void { Schema::create('pulse_values', function (Blueprint $table) { $table->engine = 'InnoDB'; + $table->id(); $table->unsignedInteger('timestamp'); $table->string('type'); $table->text('key'); @@ -35,6 +36,7 @@ public function up(): void Schema::create('pulse_entries', function (Blueprint $table) { $table->engine = 'InnoDB'; + $table->id(); $table->unsignedInteger('timestamp'); $table->string('type'); $table->text('key'); @@ -49,6 +51,7 @@ public function up(): void Schema::create('pulse_aggregates', function (Blueprint $table) { $table->engine = 'InnoDB'; + $table->id(); $table->unsignedInteger('bucket'); $table->unsignedMediumInteger('period'); $table->string('type'); diff --git a/tests/Pest.php b/tests/Pest.php index 505e902a..ecd4e1bf 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -55,6 +55,10 @@ expect()->extend('toContainAggregateForAllPeriods', function (string|array $type, string $aggregate, string $key, int $value, int $count = null, int $timestamp = null) { $this->toBeInstanceOf(Collection::class); + $values = $this->value->each(function (stdClass $value) { + unset($value->id); + }); + $types = (array) $type; $timestamp ??= now()->timestamp; From 3d88255a3a0389fa5cf71c6b922e86defa35ae8b Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Tue, 5 Dec 2023 22:45:27 +0800 Subject: [PATCH 027/110] [1.x] Redis Support Improvements (#125) * [1.x] Allow to skip Redis tests when unable to use `redis-cli`. Signed-off-by: Mior Muhammad Zaki * wip Signed-off-by: Mior Muhammad Zaki * Fix code styling * wip Signed-off-by: Mior Muhammad Zaki * Fix code styling * wip Signed-off-by: Mior Muhammad Zaki * wip * wip * wip Signed-off-by: Mior Muhammad Zaki * wip * wip Signed-off-by: Mior Muhammad Zaki * Fix code styling * wip --------- Signed-off-by: Mior Muhammad Zaki Co-authored-by: crynobone Co-authored-by: timacdonald Co-authored-by: Tim MacDonald --- .gitattributes | 2 ++ tests/Feature/RedisTest.php | 9 ++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.gitattributes b/.gitattributes index ca8cb283..e4888c62 100644 --- a/.gitattributes +++ b/.gitattributes @@ -9,10 +9,12 @@ /.github export-ignore /art export-ignore /tests export-ignore +/workbench export-ignore .editorconfig export-ignore .gitattributes export-ignore .gitignore export-ignore CHANGELOG.md export-ignore phpstan.neon.dist export-ignore phpunit.xml.dist export-ignore +testbench.yaml export-ignore UPGRADE.md export-ignore diff --git a/tests/Feature/RedisTest.php b/tests/Feature/RedisTest.php index d8b2d36f..d688f23a 100644 --- a/tests/Feature/RedisTest.php +++ b/tests/Feature/RedisTest.php @@ -1,5 +1,6 @@ Process::timeout(1)->run('redis-cli -p '.Config::get('database.redis.default.port').' FLUSHALL')->throw()); +beforeEach(function () { + try { + Process::timeout(1)->run('redis-cli -p '.Config::get('database.redis.default.port').' FLUSHALL')->throw(); + } catch (ProcessFailedException $e) { + $this->markTestSkipped('Unable to run `redis-cli`'); + } +}); it('runs the same commands while ingesting entries', function ($driver) { Config::set('database.redis.client', $driver); From 889bb44d281a689c5b769862db85e71722e89816 Mon Sep 17 00:00:00 2001 From: Tobias Oitzinger <42447585+toitzi@users.noreply.github.com> Date: Tue, 5 Dec 2023 15:45:59 +0100 Subject: [PATCH 028/110] Allow ignoring route registration (#80) Signed-off-by: Tobias Oitzinger --- src/Pulse.php | 23 +++++++++++++++++++++++ src/PulseServiceProvider.php | 20 +++++++++++--------- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/src/Pulse.php b/src/Pulse.php index bcd079b1..961dc134 100644 --- a/src/Pulse.php +++ b/src/Pulse.php @@ -82,6 +82,11 @@ class Pulse */ protected bool $runsMigrations = true; + /** + * Indicates if Pulse routes will be registered. + */ + protected bool $registersRoutes = true; + /** * Handle exceptions using the given callback. * @@ -469,6 +474,24 @@ public function ignoreMigrations(): self return $this; } + /** + * Determine if Pulse may register routes. + */ + public function registersRoutes(): bool + { + return $this->registersRoutes; + } + + /** + * Configure Pulse to not register its routes. + */ + public function ignoreRoutes(): self + { + $this->registersRoutes = false; + + return $this; + } + /** * Handle exceptions using the given callback. * diff --git a/src/PulseServiceProvider.php b/src/PulseServiceProvider.php index 7d31aea4..17e15089 100644 --- a/src/PulseServiceProvider.php +++ b/src/PulseServiceProvider.php @@ -96,15 +96,17 @@ protected function registerAuthorization(): void protected function registerRoutes(): void { $this->callAfterResolving('router', function (Router $router, Application $app) { - $router->group([ - 'domain' => $app->make('config')->get('pulse.domain', null), - 'prefix' => $app->make('config')->get('pulse.path'), - 'middleware' => $app->make('config')->get('pulse.middleware', 'web'), - ], function (Router $router) { - $router->get('/', function (Pulse $pulse, ViewFactory $view) { - return $view->make('pulse::dashboard'); - })->name('pulse'); - }); + if ($app->make(Pulse::class)->registersRoutes()) { + $router->group([ + 'domain' => $app->make('config')->get('pulse.domain', null), + 'prefix' => $app->make('config')->get('pulse.path'), + 'middleware' => $app->make('config')->get('pulse.middleware', 'web'), + ], function (Router $router) { + $router->get('/', function (Pulse $pulse, ViewFactory $view) { + return $view->make('pulse::dashboard'); + })->name('pulse'); + }); + } }); } From 1f58a957531e948d39c650bfb4ec0bbb063ccea4 Mon Sep 17 00:00:00 2001 From: taylorotwell Date: Tue, 5 Dec 2023 14:46:32 +0000 Subject: [PATCH 029/110] Update facade docblocks --- src/Facades/Pulse.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Facades/Pulse.php b/src/Facades/Pulse.php index aefb1728..1393f1ee 100644 --- a/src/Facades/Pulse.php +++ b/src/Facades/Pulse.php @@ -28,6 +28,8 @@ * @method static string js() * @method static bool runsMigrations() * @method static \Laravel\Pulse\Pulse ignoreMigrations() + * @method static bool registersRoutes() + * @method static \Laravel\Pulse\Pulse ignoreRoutes() * @method static \Laravel\Pulse\Pulse handleExceptionsUsing(callable $callback) * @method static void rescue(callable $callback) * @method static \Laravel\Pulse\Pulse setContainer(\Illuminate\Contracts\Foundation\Application $container) From 23ce6d402a2bd1e0f11899d1abdef4b334b6642c Mon Sep 17 00:00:00 2001 From: Jess Archer Date: Wed, 6 Dec 2023 00:46:57 +1000 Subject: [PATCH 030/110] [1.x] Postgres support (#64) * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * Fix code styling * wip * wip * wip * wip --------- Co-authored-by: jessarcher --- .github/workflows/tests.yml | 60 +++- .../2023_06_07_000001_create_pulse_tables.php | 29 +- phpunit.xml.dist | 3 - src/Storage/DatabaseStorage.php | 282 ++++++++++++------ tests/Feature/Recorders/SlowRequestsTest.php | 46 +-- tests/Feature/Storage/DatabaseStorageTest.php | 74 +++++ tests/Pest.php | 12 +- tests/TestCase.php | 8 + 8 files changed, 389 insertions(+), 125 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 080571cd..14a758a7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,7 +10,7 @@ on: - cron: '0 0 * * *' jobs: - tests: + mysql: runs-on: ubuntu-22.04 services: @@ -18,7 +18,7 @@ jobs: image: mysql:5.7 env: MYSQL_ALLOW_EMPTY_PASSWORD: yes - MYSQL_DATABASE: testing + MYSQL_DATABASE: forge ports: - 3306:3306 options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 @@ -35,7 +35,7 @@ jobs: laravel: [10] stability: [prefer-lowest, prefer-stable] - name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} - Stability ${{ matrix.stability }} + name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} - Stability ${{ matrix.stability }} - MySQL 5.7 steps: - name: Checkout code @@ -62,3 +62,57 @@ jobs: env: DB_CONNECTION: mysql DB_USERNAME: root + + pgsql: + runs-on: ubuntu-22.04 + + services: + postgresql: + image: postgres:14 + env: + POSTGRES_DB: forge + POSTGRES_USER: forge + POSTGRES_PASSWORD: password + ports: + - 5432:5432 + options: --health-cmd=pg_isready --health-interval=10s --health-timeout=5s --health-retries=3 + redis: + image: redis + ports: + - 6379:6379 + options: --entrypoint redis-server + + strategy: + fail-fast: true + matrix: + php: [8.2, 8.3] + laravel: [10] + + name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} - Stability ${{ matrix.stability }} - PostgreSQL 14 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: dom, curl, libxml, mbstring, redis, pcntl, zip + ini-values: error_reporting=E_ALL + tools: composer:v2 + coverage: none + + - name: Install redis-cli + run: sudo apt-get install -qq redis-tools + + - name: Install dependencies + run: | + composer require "illuminate/contracts=^${{ matrix.laravel }}" --dev --no-update + composer update --prefer-dist --no-interaction --no-progress + + - name: Execute tests + run: vendor/bin/pest + env: + DB_CONNECTION: pgsql + DB_PASSWORD: password 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 ee244372..6c845989 100644 --- a/database/migrations/2023_06_07_000001_create_pulse_tables.php +++ b/database/migrations/2023_06_07_000001_create_pulse_tables.php @@ -3,6 +3,7 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Config; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; return new class extends Migration @@ -20,27 +21,37 @@ public function getConnection(): ?string */ public function up(): void { - Schema::create('pulse_values', function (Blueprint $table) { + $connection = $this->getConnection() ?? DB::connection(); + + Schema::create('pulse_values', function (Blueprint $table) use ($connection) { $table->engine = 'InnoDB'; $table->id(); $table->unsignedInteger('timestamp'); $table->string('type'); $table->text('key'); - $table->char('key_hash', 16)->charset('binary')->virtualAs('unhex(md5(`key`))'); + match ($driver = $connection->getDriverName()) { + 'mysql' => $table->char('key_hash', 16)->charset('binary')->virtualAs('unhex(md5(`key`))'), + 'pgsql' => $table->uuid('key_hash')->storedAs('md5("key")::uuid'), + default => throw new RuntimeException("Unsupported database driver [{$driver}]."), + }; $table->text('value'); $table->index('timestamp'); // For trimming... $table->index('type'); // For fast lookups and purging... - $table->unique(['type', 'key_hash']); // For data integrity... + $table->unique(['type', 'key_hash']); // For data integrity and upserts... }); - Schema::create('pulse_entries', function (Blueprint $table) { + Schema::create('pulse_entries', function (Blueprint $table) use ($connection) { $table->engine = 'InnoDB'; $table->id(); $table->unsignedInteger('timestamp'); $table->string('type'); $table->text('key'); - $table->char('key_hash', 16)->charset('binary')->virtualAs('unhex(md5(`key`))'); + match ($driver = $connection->getDriverName()) { + 'mysql' => $table->char('key_hash', 16)->charset('binary')->virtualAs('unhex(md5(`key`))'), + 'pgsql' => $table->uuid('key_hash')->storedAs('md5("key")::uuid'), + default => throw new RuntimeException("Unsupported database driver [{$driver}]."), + }; $table->bigInteger('value')->nullable(); $table->index('timestamp'); // For trimming... @@ -49,14 +60,18 @@ public function up(): void $table->index(['timestamp', 'type', 'key_hash', 'value']); // For aggregate queries... }); - Schema::create('pulse_aggregates', function (Blueprint $table) { + Schema::create('pulse_aggregates', function (Blueprint $table) use ($connection) { $table->engine = 'InnoDB'; $table->id(); $table->unsignedInteger('bucket'); $table->unsignedMediumInteger('period'); $table->string('type'); $table->text('key'); - $table->char('key_hash', 16)->charset('binary')->virtualAs('unhex(md5(`key`))'); + match ($driver = $connection->getDriverName()) { + 'mysql' => $table->char('key_hash', 16)->charset('binary')->virtualAs('unhex(md5(`key`))'), + 'pgsql' => $table->uuid('key_hash')->storedAs('md5("key")::uuid'), + default => throw new RuntimeException("Unsupported database driver [{$driver}]."), + }; $table->string('aggregate'); $table->decimal('value', 20, 2); $table->unsignedInteger('count')->nullable(); diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 8cc657da..0749980f 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -10,8 +10,5 @@ - - - diff --git a/src/Storage/DatabaseStorage.php b/src/Storage/DatabaseStorage.php index 771fd977..66c12fcc 100644 --- a/src/Storage/DatabaseStorage.php +++ b/src/Storage/DatabaseStorage.php @@ -8,16 +8,16 @@ use Illuminate\Database\Connection; use Illuminate\Database\DatabaseManager; use Illuminate\Database\Query\Builder; -use Illuminate\Support\Arr; +use Illuminate\Database\Query\Expression; use Illuminate\Support\Collection; -use Illuminate\Support\LazyCollection; use InvalidArgumentException; use Laravel\Pulse\Contracts\Storage; use Laravel\Pulse\Entry; use Laravel\Pulse\Value; +use RuntimeException; /** - * @phpstan-type AggregateRow array{bucket: int, period: int, type: string, aggregate: string, key: string, value: int, count: int} + * @phpstan-type AggregateRow array{bucket: int, period: int, type: string, aggregate: string, key: string, value: int|float, count?: int} * * @internal */ @@ -55,25 +55,18 @@ public function store(Collection $items): void ->insert($chunk->map->attributes()->all()) ); - $periods = [ - (int) (CarbonInterval::hour()->totalSeconds / 60), - (int) (CarbonInterval::hours(6)->totalSeconds / 60), - (int) (CarbonInterval::hours(24)->totalSeconds / 60), - (int) (CarbonInterval::days(7)->totalSeconds / 60), - ]; - $this - ->aggregateAttributes($entries->filter->isCount(), $periods, 'count') + ->aggregateCounts($entries->filter->isCount()) ->chunk($this->config->get('pulse.storage.database.chunk')) ->each(fn ($chunk) => $this->upsertCount($chunk->all())); $this - ->aggregateAttributes($entries->filter->isMax(), $periods, 'max') + ->aggregateMaximums($entries->filter->isMax()) ->chunk($this->config->get('pulse.storage.database.chunk')) ->each(fn ($chunk) => $this->upsertMax($chunk->all())); $this - ->aggregateAttributes($entries->filter->isAvg(), $periods, 'avg') + ->aggregateAverages($entries->filter->isAvg()) ->chunk($this->config->get('pulse.storage.database.chunk')) ->each(fn ($chunk) => $this->upsertAvg($chunk->all())); @@ -147,11 +140,18 @@ public function purge(array $types = null): void * * @param list $values */ - protected function upsertCount(array $values): bool + protected function upsertCount(array $values): int { - return $this->upsert( + return $this->connection()->table('pulse_aggregates')->upsert( $values, - 'on duplicate key update `value` = `value` + 1' + ['bucket', 'period', 'type', 'aggregate', 'key_hash'], + [ + 'value' => match ($driver = $this->connection()->getDriverName()) { + 'mysql' => new Expression('`value` + values(`value`)'), + 'pgsql' => new Expression('"pulse_aggregates"."value" + "excluded"."value"'), + default => throw new RuntimeException("Unsupported database driver [{$driver}]"), + }, + ] ); } @@ -160,11 +160,18 @@ protected function upsertCount(array $values): bool * * @param list $values */ - protected function upsertMax(array $values): bool + protected function upsertMax(array $values): int { - return $this->upsert( + return $this->connection()->table('pulse_aggregates')->upsert( $values, - 'on duplicate key update `value` = greatest(`value`, values(`value`))' + ['bucket', 'period', 'type', 'aggregate', 'key_hash'], + [ + 'value' => match ($driver = $this->connection()->getDriverName()) { + 'mysql' => new Expression('greatest(`value`, values(`value`))'), + 'pgsql' => new Expression('greatest("pulse_aggregates"."value", "excluded"."value")'), + default => throw new RuntimeException("Unsupported database driver [{$driver}]"), + }, + ] ); } @@ -173,66 +180,157 @@ protected function upsertMax(array $values): bool * * @param list $values */ - protected function upsertAvg(array $values): bool + protected function upsertAvg(array $values): int { - return $this->upsert( + return $this->connection()->table('pulse_aggregates')->upsert( $values, - ' on duplicate key update `value` = (`value` * `count` + values(`value`)) / (`count` + 1), `count` = `count` + 1', + ['bucket', 'period', 'type', 'aggregate', 'key_hash'], + match ($driver = $this->connection()->getDriverName()) { + 'mysql' => [ + 'value' => new Expression('(`value` * `count` + (values(`value`) * values(`count`))) / (`count` + values(`count`))'), + 'count' => new Expression('`count` + values(`count`)'), + ], + 'pgsql' => [ + 'value' => new Expression('("pulse_aggregates"."value" * "pulse_aggregates"."count" + ("excluded"."value" * "excluded"."count")) / ("pulse_aggregates"."count" + "excluded"."count")'), + 'count' => new Expression('"pulse_aggregates"."count" + "excluded"."count"'), + ], + default => throw new RuntimeException("Unsupported database driver [{$driver}]"), + } ); } /** - * Perform an "upsert" query with an "on duplicate key" clause. + * Get the count aggregates * - * @param list $values + * @param \Illuminate\Support\Collection $entries + * @return \Illuminate\Support\Collection */ - protected function upsert(array $values, string $onDuplicateKeyClause): bool + protected function aggregateCounts(Collection $entries): Collection { - $grammar = $this->connection()->getQueryGrammar(); + $aggregates = []; - $sql = $grammar->compileInsert( - $this->connection()->table('pulse_aggregates'), - $values - ); + foreach ($entries as $entry) { + foreach ($this->periods() as $period) { + // Exclude entries that would be trimmed. + if ($entry->timestamp < CarbonImmutable::now()->subMinutes($period)->getTimestamp()) { + continue; + } - $sql .= ' '.$onDuplicateKeyClause; + $bucket = (int) (floor($entry->timestamp / $period) * $period); + + $key = $entry->type.':'.$period.':'.$bucket.':'.$entry->key; + + if (! isset($aggregates[$key])) { + $aggregates[$key] = [ + 'bucket' => $bucket, + 'period' => $period, + 'type' => $entry->type, + 'aggregate' => 'count', + 'key' => $entry->key, + 'value' => 1, + ]; + } else { + $aggregates[$key]['value']++; + } + } + } - return $this->connection()->statement($sql, Arr::flatten($values, 1)); + return collect(array_values($aggregates)); } /** - * Get the aggregate attributes for the collection. + * Get the maximum aggregates * * @param \Illuminate\Support\Collection $entries - * @param list $periods - * @return \Illuminate\Support\LazyCollection + * @return \Illuminate\Support\Collection */ - protected function aggregateAttributes(Collection $entries, array $periods, string $aggregateSuffix): LazyCollection + protected function aggregateMaximums(Collection $entries): Collection { - return LazyCollection::make(function () use ($entries, $periods, $aggregateSuffix) { - foreach ($entries as $entry) { - foreach ($periods as $period) { - // Exclude entries that would be trimmed. - if ($entry->timestamp < CarbonImmutable::now()->subMinutes($period)->getTimestamp()) { - continue; - } + $aggregates = []; + + foreach ($entries as $entry) { + foreach ($this->periods() as $period) { + // Exclude entries that would be trimmed. + if ($entry->timestamp < CarbonImmutable::now()->subMinutes($period)->getTimestamp()) { + continue; + } + + $bucket = (int) (floor($entry->timestamp / $period) * $period); + + $key = $entry->type.':'.$period.':'.$bucket.':'.$entry->key; - yield [ - 'bucket' => (int) (floor($entry->timestamp / $period) * $period), + if (! isset($aggregates[$key])) { + $aggregates[$key] = [ + 'bucket' => $bucket, 'period' => $period, 'type' => $entry->type, - 'aggregate' => $aggregateSuffix, + 'aggregate' => 'max', 'key' => $entry->key, - 'value' => $aggregateSuffix === 'count' - ? 1 - : $entry->value, - ...($aggregateSuffix === 'avg') - ? ['count' => 1] - : [], + 'value' => (int) $entry->value, ]; + } else { + $aggregates[$key]['value'] = (int) max($aggregates[$key]['value'], $entry->value); } } - }); + } + + return collect(array_values($aggregates)); + } + + /** + * Get the average aggregates + * + * @param \Illuminate\Support\Collection $entries + * @return \Illuminate\Support\Collection + */ + protected function aggregateAverages(Collection $entries): Collection + { + $aggregates = []; + + foreach ($entries as $entry) { + foreach ($this->periods() as $period) { + // Exclude entries that would be trimmed. + if ($entry->timestamp < CarbonImmutable::now()->subMinutes($period)->getTimestamp()) { + continue; + } + + $bucket = (int) (floor($entry->timestamp / $period) * $period); + + $key = $entry->type.':'.$period.':'.$bucket.':'.$entry->key; + + if (! isset($aggregates[$key])) { + $aggregates[$key] = [ + 'bucket' => $bucket, + 'period' => $period, + 'type' => $entry->type, + 'aggregate' => 'avg', + 'key' => $entry->key, + 'value' => (int) $entry->value, + 'count' => 1, + ]; + } else { + $aggregates[$key]['value'] = ($aggregates[$key]['value'] * $aggregates[$key]['count'] + $entry->value) / ($aggregates[$key]['count'] + 1); + $aggregates[$key]['count']++; + } + } + } + + return collect(array_values($aggregates)); + } + + /** + * The periods to aggregate for. + * + * @return list + */ + protected function periods(): array + { + return [ + (int) (CarbonInterval::hour()->totalSeconds / 60), + (int) (CarbonInterval::hours(6)->totalSeconds / 60), + (int) (CarbonInterval::hours(24)->totalSeconds / 60), + (int) (CarbonInterval::days(7)->totalSeconds / 60), + ]; } /** @@ -346,10 +444,10 @@ public function aggregate( foreach ($aggregates as $aggregate) { $query->selectRaw(match ($aggregate) { - 'count' => 'sum(`count`)', - 'max' => 'max(`max`)', - 'avg' => 'avg(`avg`)', - }." as `{$aggregate}`"); + 'count' => "sum({$this->wrap('count')})", + 'max' => "max({$this->wrap('max')})", + 'avg' => "avg({$this->wrap('avg')})", + }." as {$this->wrap($aggregate)}"); } $query->fromSub(function (Builder $query) use ($type, $aggregates, $interval) { @@ -365,9 +463,9 @@ public function aggregate( foreach ($aggregates as $aggregate) { $query->selectRaw(match ($aggregate) { 'count' => 'count(*)', - 'max' => 'max(`value`)', - 'avg' => 'avg(`value`)', - }." as `{$aggregate}`"); + 'max' => "max({$this->wrap('value')})", + 'avg' => "avg({$this->wrap('value')})", + }." as {$this->wrap($aggregate)}"); } $query @@ -385,12 +483,12 @@ public function aggregate( foreach ($aggregates as $aggregate) { if ($aggregate === $currentAggregate) { $query->selectRaw(match ($aggregate) { - 'count' => 'sum(`value`)', - 'max' => 'max(`value`)', - 'avg' => 'avg(`value`)', - }." as `$aggregate`"); + 'count' => "sum({$this->wrap('value')})", + 'max' => "max({$this->wrap('value')})", + 'avg' => "avg({$this->wrap('value')})", + }." as {$this->wrap($aggregate)}"); } else { - $query->selectRaw("null as `$aggregate`"); + $query->selectRaw("null as {$this->wrap($aggregate)}"); } } @@ -448,10 +546,10 @@ public function aggregateTypes( foreach ($types as $type) { $query->selectRaw(match ($aggregate) { - 'count' => 'sum(`'.$type.'`)', - 'max' => 'max(`'.$type.'`)', - 'avg' => 'avg(`'.$type.'`)', - }." as `{$type}`"); + 'count' => "sum({$this->wrap($type)})", + 'max' => "max({$this->wrap($type)})", + 'avg' => "avg({$this->wrap($type)})", + }." as {$this->wrap($type)}"); } $query->fromSub(function (Builder $query) use ($types, $aggregate, $interval) { @@ -466,10 +564,10 @@ public function aggregateTypes( foreach ($types as $type) { $query->selectRaw(match ($aggregate) { - 'count' => 'count(case when (`type` = ?) then true else null end)', - 'max' => 'max(case when (`type` = ?) then `value` else null end)', - 'avg' => 'avg(case when (`type` = ?) then `value` else null end)', - }." as `{$type}`", [$type]); + 'count' => "count(case when ({$this->wrap('type')} = ?) then true else null end)", + 'max' => "max(case when ({$this->wrap('type')} = ?) then {$this->wrap('value')} else null end)", + 'avg' => "avg(case when ({$this->wrap('type')} = ?) then {$this->wrap('value')} else null end)", + }." as {$this->wrap($type)}", [$type]); } $query @@ -485,10 +583,10 @@ public function aggregateTypes( foreach ($types as $type) { $query->selectRaw(match ($aggregate) { - 'count' => 'sum(case when (`type` = ?) then `value` else null end)', - 'max' => 'max(case when (`type` = ?) then `value` else null end)', - 'avg' => 'avg(case when (`type` = ?) then `value` else null end)', - }." as `{$type}`", [$type]); + 'count' => "sum(case when ({$this->wrap('type')} = ?) then {$this->wrap('value')} else null end)", + 'max' => "max(case when ({$this->wrap('type')} = ?) then {$this->wrap('value')} else null end)", + 'avg' => "avg(case when ({$this->wrap('type')} = ?) then {$this->wrap('value')} else null end)", + }." as {$this->wrap($type)}", [$type]); } $query @@ -536,18 +634,18 @@ public function aggregateTotal( return $this->connection()->query() ->addSelect('type') ->selectRaw(match ($aggregate) { - 'count' => 'sum(`count`)', - 'max' => 'max(`max`)', - 'avg' => 'avg(`avg`)', - }." as `{$aggregate}`") + 'count' => "sum({$this->wrap('count')})", + 'max' => "max({$this->wrap('max')})", + 'avg' => "avg({$this->wrap('avg')})", + }." as {$this->wrap($aggregate)}") ->fromSub(fn (Builder $query) => $query // Tail ->addSelect('type') ->selectRaw(match ($aggregate) { 'count' => 'count(*)', - 'max' => 'max(`value`)', - 'avg' => 'avg(`value`)', - }." as `{$aggregate}`") + 'max' => "max({$this->wrap('value')})", + 'avg' => "avg({$this->wrap('value')})", + }." as {$this->wrap($aggregate)}") ->from('pulse_entries') ->whereIn('type', $types) ->where('timestamp', '>=', $tailStart) @@ -557,10 +655,10 @@ public function aggregateTotal( ->unionAll(fn (Builder $query) => $query ->select('type') ->selectRaw(match ($aggregate) { - 'count' => 'sum(`value`)', - 'max' => 'max(`value`)', - 'avg' => 'avg(`value`)', - }."as `{$aggregate}`") + 'count' => "sum({$this->wrap('value')})", + 'max' => "max({$this->wrap('value')})", + 'avg' => "avg({$this->wrap('value')})", + }." as {$this->wrap($aggregate)}") ->from('pulse_aggregates') ->where('period', $period) ->whereIn('type', $types) @@ -581,4 +679,12 @@ protected function connection(): Connection { return $this->db->connection($this->config->get('pulse.storage.database.connection')); } + + /** + * Wrap a value in keyword identifiers. + */ + protected function wrap(string $value): string + { + return $this->connection()->getQueryGrammar()->wrap($value); + } } diff --git a/tests/Feature/Recorders/SlowRequestsTest.php b/tests/Feature/Recorders/SlowRequestsTest.php index b0e9f38a..6e93ed48 100644 --- a/tests/Feature/Recorders/SlowRequestsTest.php +++ b/tests/Feature/Recorders/SlowRequestsTest.php @@ -28,7 +28,7 @@ expect($entries[0]->timestamp)->toBe(946782245); expect($entries[0]->type)->toBe('slow_request'); expect($entries[0]->key)->toBe(json_encode(['GET', '/test-route', 'Closure'])); - expect($entries[0]->key_hash)->toBe(hex2bin(md5(json_encode(['GET', '/test-route', 'Closure'])))); + expect($entries[0]->key_hash)->toBe(keyHash(json_encode(['GET', '/test-route', 'Closure']))); expect($entries[0]->value)->toBe(4000); $aggregates = Pulse::ignore(fn () => DB::table('pulse_aggregates')->orderBy('type')->orderBy('period')->orderBy('aggregate')->get()); @@ -39,7 +39,7 @@ expect($aggregates[0]->type)->toBe('slow_request'); expect($aggregates[0]->aggregate)->toBe('count'); expect($aggregates[0]->key)->toBe(json_encode(['GET', '/test-route', 'Closure'])); - expect($aggregates[0]->key_hash)->toBe(hex2bin(md5(json_encode(['GET', '/test-route', 'Closure'])))); + expect($aggregates[0]->key_hash)->toBe(keyHash(json_encode(['GET', '/test-route', 'Closure']))); expect($aggregates[0]->value)->toBe('1.00'); expect($aggregates[1]->bucket)->toBe(946782240); @@ -47,7 +47,7 @@ expect($aggregates[1]->type)->toBe('slow_request'); expect($aggregates[1]->aggregate)->toBe('max'); expect($aggregates[1]->key)->toBe(json_encode(['GET', '/test-route', 'Closure'])); - expect($aggregates[1]->key_hash)->toBe(hex2bin(md5(json_encode(['GET', '/test-route', 'Closure'])))); + expect($aggregates[1]->key_hash)->toBe(keyHash(json_encode(['GET', '/test-route', 'Closure']))); expect($aggregates[1]->value)->toBe('4000.00'); expect($aggregates[2]->bucket)->toBe(946782000); @@ -55,7 +55,7 @@ expect($aggregates[2]->type)->toBe('slow_request'); expect($aggregates[2]->aggregate)->toBe('count'); expect($aggregates[2]->key)->toBe(json_encode(['GET', '/test-route', 'Closure'])); - expect($aggregates[2]->key_hash)->toBe(hex2bin(md5(json_encode(['GET', '/test-route', 'Closure'])))); + expect($aggregates[2]->key_hash)->toBe(keyHash(json_encode(['GET', '/test-route', 'Closure']))); expect($aggregates[2]->value)->toBe('1.00'); expect($aggregates[3]->bucket)->toBe(946782000); @@ -63,7 +63,7 @@ expect($aggregates[3]->type)->toBe('slow_request'); expect($aggregates[3]->aggregate)->toBe('max'); expect($aggregates[3]->key)->toBe(json_encode(['GET', '/test-route', 'Closure'])); - expect($aggregates[3]->key_hash)->toBe(hex2bin(md5(json_encode(['GET', '/test-route', 'Closure'])))); + expect($aggregates[3]->key_hash)->toBe(keyHash(json_encode(['GET', '/test-route', 'Closure']))); expect($aggregates[3]->value)->toBe('4000.00'); expect($aggregates[4]->bucket)->toBe(946781280); @@ -71,7 +71,7 @@ expect($aggregates[4]->type)->toBe('slow_request'); expect($aggregates[4]->aggregate)->toBe('count'); expect($aggregates[4]->key)->toBe(json_encode(['GET', '/test-route', 'Closure'])); - expect($aggregates[4]->key_hash)->toBe(hex2bin(md5(json_encode(['GET', '/test-route', 'Closure'])))); + expect($aggregates[4]->key_hash)->toBe(keyHash(json_encode(['GET', '/test-route', 'Closure']))); expect($aggregates[4]->value)->toBe('1.00'); expect($aggregates[5]->bucket)->toBe(946781280); @@ -79,21 +79,21 @@ expect($aggregates[5]->type)->toBe('slow_request'); expect($aggregates[5]->aggregate)->toBe('max'); expect($aggregates[5]->key)->toBe(json_encode(['GET', '/test-route', 'Closure'])); - expect($aggregates[5]->key_hash)->toBe(hex2bin(md5(json_encode(['GET', '/test-route', 'Closure'])))); + expect($aggregates[5]->key_hash)->toBe(keyHash(json_encode(['GET', '/test-route', 'Closure']))); expect($aggregates[5]->value)->toBe('4000.00'); expect($aggregates[6]->period)->toBe(10080); expect($aggregates[6]->type)->toBe('slow_request'); expect($aggregates[6]->aggregate)->toBe('count'); expect($aggregates[6]->key)->toBe(json_encode(['GET', '/test-route', 'Closure'])); - expect($aggregates[6]->key_hash)->toBe(hex2bin(md5(json_encode(['GET', '/test-route', 'Closure'])))); + expect($aggregates[6]->key_hash)->toBe(keyHash(json_encode(['GET', '/test-route', 'Closure']))); expect($aggregates[6]->value)->toBe('1.00'); expect($aggregates[7]->period)->toBe(10080); expect($aggregates[7]->type)->toBe('slow_request'); expect($aggregates[7]->aggregate)->toBe('max'); expect($aggregates[7]->key)->toBe(json_encode(['GET', '/test-route', 'Closure'])); - expect($aggregates[7]->key_hash)->toBe(hex2bin(md5(json_encode(['GET', '/test-route', 'Closure'])))); + expect($aggregates[7]->key_hash)->toBe(keyHash(json_encode(['GET', '/test-route', 'Closure']))); expect($aggregates[7]->value)->toBe('4000.00'); Pulse::ignore(fn () => expect(DB::table('pulse_values')->count())->toBe(0)); @@ -114,7 +114,7 @@ expect($entries[0]->timestamp)->toBe(946782245); expect($entries[0]->type)->toBe('slow_user_request'); expect($entries[0]->key)->toBe('4321'); - expect($entries[0]->key_hash)->toBe(hex2bin(md5('4321'))); + expect($entries[0]->key_hash)->toBe(keyHash('4321')); expect($entries[0]->value)->toBeNull(); $aggregates = Pulse::ignore(fn () => DB::table('pulse_aggregates')->where('type', 'slow_user_request')->orderBy('period')->orderBy('aggregate')->get()); @@ -125,7 +125,7 @@ expect($aggregates[0]->type)->toBe('slow_user_request'); expect($aggregates[0]->aggregate)->toBe('count'); expect($aggregates[0]->key)->toBe('4321'); - expect($aggregates[0]->key_hash)->toBe(hex2bin(md5('4321'))); + expect($aggregates[0]->key_hash)->toBe(keyHash('4321')); expect($aggregates[0]->value)->toBe('1.00'); expect($aggregates[1]->bucket)->toBe(946782000); @@ -133,7 +133,7 @@ expect($aggregates[1]->type)->toBe('slow_user_request'); expect($aggregates[1]->aggregate)->toBe('count'); expect($aggregates[1]->key)->toBe('4321'); - expect($aggregates[1]->key_hash)->toBe(hex2bin(md5('4321'))); + expect($aggregates[1]->key_hash)->toBe(keyHash('4321')); expect($aggregates[1]->value)->toBe('1.00'); expect($aggregates[2]->bucket)->toBe(946781280); @@ -141,14 +141,14 @@ expect($aggregates[2]->type)->toBe('slow_user_request'); expect($aggregates[2]->aggregate)->toBe('count'); expect($aggregates[2]->key)->toBe('4321'); - expect($aggregates[2]->key_hash)->toBe(hex2bin(md5('4321'))); + expect($aggregates[2]->key_hash)->toBe(keyHash('4321')); expect($aggregates[2]->value)->toBe('1.00'); expect($aggregates[3]->period)->toBe(10080); expect($aggregates[3]->type)->toBe('slow_user_request'); expect($aggregates[3]->aggregate)->toBe('count'); expect($aggregates[3]->key)->toBe('4321'); - expect($aggregates[3]->key_hash)->toBe(hex2bin(md5('4321'))); + expect($aggregates[3]->key_hash)->toBe(keyHash('4321')); expect($aggregates[3]->value)->toBe('1.00'); Pulse::ignore(fn () => expect(DB::table('pulse_values')->count())->toBe(0)); @@ -268,7 +268,7 @@ expect($entries[0]->timestamp)->toBe(946782245); expect($entries[0]->type)->toBe('slow_request'); expect($entries[0]->key)->toBe(json_encode(['POST', '/test-route', 'via /livewire/update'])); - expect($entries[0]->key_hash)->toBe(hex2bin(md5(json_encode(['POST', '/test-route', 'via /livewire/update'])))); + expect($entries[0]->key_hash)->toBe(keyHash(json_encode(['POST', '/test-route', 'via /livewire/update']))); expect($entries[0]->value)->toBe(4000); $aggregates = Pulse::ignore(fn () => DB::table('pulse_aggregates')->orderBy('type')->orderBy('period')->orderBy('aggregate')->get()); @@ -279,7 +279,7 @@ expect($aggregates[0]->type)->toBe('slow_request'); expect($aggregates[0]->aggregate)->toBe('count'); expect($aggregates[0]->key)->toBe(json_encode(['POST', '/test-route', 'via /livewire/update'])); - expect($aggregates[0]->key_hash)->toBe(hex2bin(md5(json_encode(['POST', '/test-route', 'via /livewire/update'])))); + expect($aggregates[0]->key_hash)->toBe(keyHash(json_encode(['POST', '/test-route', 'via /livewire/update']))); expect($aggregates[0]->value)->toBe('1.00'); expect($aggregates[1]->bucket)->toBe(946782240); @@ -287,7 +287,7 @@ expect($aggregates[1]->type)->toBe('slow_request'); expect($aggregates[1]->aggregate)->toBe('max'); expect($aggregates[1]->key)->toBe(json_encode(['POST', '/test-route', 'via /livewire/update'])); - expect($aggregates[1]->key_hash)->toBe(hex2bin(md5(json_encode(['POST', '/test-route', 'via /livewire/update'])))); + expect($aggregates[1]->key_hash)->toBe(keyHash(json_encode(['POST', '/test-route', 'via /livewire/update']))); expect($aggregates[1]->value)->toBe('4000.00'); expect($aggregates[2]->bucket)->toBe(946782000); @@ -295,7 +295,7 @@ expect($aggregates[2]->type)->toBe('slow_request'); expect($aggregates[2]->aggregate)->toBe('count'); expect($aggregates[2]->key)->toBe(json_encode(['POST', '/test-route', 'via /livewire/update'])); - expect($aggregates[2]->key_hash)->toBe(hex2bin(md5(json_encode(['POST', '/test-route', 'via /livewire/update'])))); + expect($aggregates[2]->key_hash)->toBe(keyHash(json_encode(['POST', '/test-route', 'via /livewire/update']))); expect($aggregates[2]->value)->toBe('1.00'); expect($aggregates[3]->bucket)->toBe(946782000); @@ -303,7 +303,7 @@ expect($aggregates[3]->type)->toBe('slow_request'); expect($aggregates[3]->aggregate)->toBe('max'); expect($aggregates[3]->key)->toBe(json_encode(['POST', '/test-route', 'via /livewire/update'])); - expect($aggregates[3]->key_hash)->toBe(hex2bin(md5(json_encode(['POST', '/test-route', 'via /livewire/update'])))); + expect($aggregates[3]->key_hash)->toBe(keyHash(json_encode(['POST', '/test-route', 'via /livewire/update']))); expect($aggregates[3]->value)->toBe('4000.00'); expect($aggregates[4]->bucket)->toBe(946781280); @@ -311,7 +311,7 @@ expect($aggregates[4]->type)->toBe('slow_request'); expect($aggregates[4]->aggregate)->toBe('count'); expect($aggregates[4]->key)->toBe(json_encode(['POST', '/test-route', 'via /livewire/update'])); - expect($aggregates[4]->key_hash)->toBe(hex2bin(md5(json_encode(['POST', '/test-route', 'via /livewire/update'])))); + expect($aggregates[4]->key_hash)->toBe(keyHash(json_encode(['POST', '/test-route', 'via /livewire/update']))); expect($aggregates[4]->value)->toBe('1.00'); expect($aggregates[5]->bucket)->toBe(946781280); @@ -319,7 +319,7 @@ expect($aggregates[5]->type)->toBe('slow_request'); expect($aggregates[5]->aggregate)->toBe('max'); expect($aggregates[5]->key)->toBe(json_encode(['POST', '/test-route', 'via /livewire/update'])); - expect($aggregates[5]->key_hash)->toBe(hex2bin(md5(json_encode(['POST', '/test-route', 'via /livewire/update'])))); + expect($aggregates[5]->key_hash)->toBe(keyHash(json_encode(['POST', '/test-route', 'via /livewire/update']))); expect($aggregates[5]->value)->toBe('4000.00'); expect($aggregates[6]->bucket)->toBe(946774080); @@ -327,7 +327,7 @@ expect($aggregates[6]->type)->toBe('slow_request'); expect($aggregates[6]->aggregate)->toBe('count'); expect($aggregates[6]->key)->toBe(json_encode(['POST', '/test-route', 'via /livewire/update'])); - expect($aggregates[6]->key_hash)->toBe(hex2bin(md5(json_encode(['POST', '/test-route', 'via /livewire/update'])))); + expect($aggregates[6]->key_hash)->toBe(keyHash(json_encode(['POST', '/test-route', 'via /livewire/update']))); expect($aggregates[6]->value)->toBe('1.00'); expect($aggregates[7]->bucket)->toBe(946774080); @@ -335,7 +335,7 @@ expect($aggregates[7]->type)->toBe('slow_request'); expect($aggregates[7]->aggregate)->toBe('max'); expect($aggregates[7]->key)->toBe(json_encode(['POST', '/test-route', 'via /livewire/update'])); - expect($aggregates[7]->key_hash)->toBe(hex2bin(md5(json_encode(['POST', '/test-route', 'via /livewire/update'])))); + expect($aggregates[7]->key_hash)->toBe(keyHash(json_encode(['POST', '/test-route', 'via /livewire/update']))); expect($aggregates[7]->value)->toBe('4000.00'); Pulse::ignore(fn () => expect(DB::table('pulse_values')->count())->toBe(0)); diff --git a/tests/Feature/Storage/DatabaseStorageTest.php b/tests/Feature/Storage/DatabaseStorageTest.php index 3b783317..93fc05c4 100644 --- a/tests/Feature/Storage/DatabaseStorageTest.php +++ b/tests/Feature/Storage/DatabaseStorageTest.php @@ -2,8 +2,82 @@ use Carbon\CarbonInterval; use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\DB; use Laravel\Pulse\Facades\Pulse; +it('combines duplicate count aggregates before upserting', function () { + $queries = collect(); + DB::listen(fn ($query) => $queries[] = $query); + + Pulse::record('type', 'key1')->count(); + Pulse::record('type', 'key1')->count(); + Pulse::record('type', 'key1')->count(); + Pulse::record('type', 'key2')->count(); + Pulse::store(); + + expect($queries)->toHaveCount(2); + expect($queries[0]->sql)->toContain('pulse_entries'); + expect($queries[1]->sql)->toContain('pulse_aggregates'); + expect($queries[0]->bindings)->toHaveCount(4 * 4); // 4 entries, 4 columns each + expect($queries[1]->bindings)->toHaveCount(2 * 6 * 4); // 2 entries, 6 columns each, 4 periods + + $aggregates = Pulse::ignore(fn () => DB::table('pulse_aggregates')->where('period', 60)->orderBy('key')->pluck('value', 'key')); + expect($aggregates['key1'])->toEqual(3); + expect($aggregates['key2'])->toEqual(1); +}); + +it('combines duplicate max aggregates before upserting', function () { + $queries = collect(); + DB::listen(fn ($query) => $queries[] = $query); + + Pulse::record('type', 'key1', 100)->max(); + Pulse::record('type', 'key1', 300)->max(); + Pulse::record('type', 'key1', 200)->max(); + Pulse::record('type', 'key2', 100)->max(); + Pulse::store(); + + expect($queries)->toHaveCount(2); + expect($queries[0]->sql)->toContain('pulse_entries'); + expect($queries[1]->sql)->toContain('pulse_aggregates'); + expect($queries[0]->bindings)->toHaveCount(4 * 4); // 4 entries, 4 columns each + expect($queries[1]->bindings)->toHaveCount(2 * 6 * 4); // 2 entries, 6 columns each, 4 periods + + $aggregates = Pulse::ignore(fn () => DB::table('pulse_aggregates')->where('period', 60)->orderBy('key')->pluck('value', 'key')); + expect($aggregates['key1'])->toEqual(300); + expect($aggregates['key2'])->toEqual(100); +}); + +it('combines duplicate average aggregates before upserting', function () { + $queries = collect(); + DB::listen(fn ($query) => $queries[] = $query); + + Pulse::record('type', 'key1', 100)->avg(); + Pulse::record('type', 'key1', 300)->avg(); + Pulse::record('type', 'key1', 200)->avg(); + Pulse::record('type', 'key2', 100)->avg(); + Pulse::store(); + + expect($queries)->toHaveCount(2); + expect($queries[0]->sql)->toContain('pulse_entries'); + expect($queries[1]->sql)->toContain('pulse_aggregates'); + expect($queries[0]->bindings)->toHaveCount(4 * 4); // 4 entries, 4 columns each + expect($queries[1]->bindings)->toHaveCount(2 * 7 * 4); // 2 entries, 7 columns each, 4 periods + + $aggregates = Pulse::ignore(fn () => DB::table('pulse_aggregates')->where('period', 60)->orderBy('key')->get())->keyBy('key'); + expect($aggregates['key1']->value)->toEqual(200); + expect($aggregates['key2']->value)->toEqual(100); + expect($aggregates['key1']->count)->toEqual(3); + expect($aggregates['key2']->count)->toEqual(1); + + Pulse::record('type', 'key1', 400)->avg(); + Pulse::record('type', 'key1', 400)->avg(); + Pulse::record('type', 'key1', 400)->avg(); + Pulse::store(); + $aggregate = Pulse::ignore(fn () => DB::table('pulse_aggregates')->where('period', 60)->where('key', 'key1')->first()); + expect($aggregate->count)->toEqual(6); + expect($aggregate->value)->toEqual(300); +}); + test('one or more aggregates for a single type', function () { /* | key | max | avg | count | diff --git a/tests/Pest.php b/tests/Pest.php index ecd4e1bf..0694db1a 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -4,6 +4,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Config; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Process; @@ -11,6 +12,7 @@ use Illuminate\Support\Str; use Laravel\Pulse\Facades\Pulse; use PHPUnit\Framework\Assert; +use Ramsey\Uuid\Uuid; use Tests\TestCase; /* @@ -72,7 +74,7 @@ 'type' => $type, 'aggregate' => $aggregate, 'key' => $key, - 'key_hash' => hex2bin(md5($key)), + 'key_hash' => keyHash($key), 'value' => $value, 'count' => $count, ]; @@ -95,6 +97,14 @@ | */ +function keyHash(string $string): string +{ + return match (DB::connection()->getDriverName()) { + 'mysql' => hex2bin(md5($string)), + 'pgsql' => Uuid::fromString(md5($string)), + }; +} + function prependListener(string $event, callable $listener): void { $listeners = Event::getRawListeners()[$event]; diff --git a/tests/TestCase.php b/tests/TestCase.php index 2de70fbd..08867bda 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,6 +2,7 @@ namespace Tests; +use Illuminate\Contracts\Config\Repository; use Illuminate\Foundation\Testing\RefreshDatabase; use Orchestra\Testbench\TestCase as OrchestraTestCase; @@ -22,4 +23,11 @@ protected function defineDatabaseMigrations(): void { $this->loadMigrationsFrom(__DIR__.'/migrations'); } + + protected function defineEnvironment($app): void + { + tap($app['config'], function (Repository $config) { + $config->set('queue.failed.driver', 'null'); + }); + } } From 31094faac2b1ecdbf881428a993e663f19dbceff Mon Sep 17 00:00:00 2001 From: Mohamed Benhida Date: Tue, 5 Dec 2023 23:27:11 +0100 Subject: [PATCH 031/110] fix connection returning string (#149) --- database/migrations/2023_06_07_000001_create_pulse_tables.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 6c845989..9f06312e 100644 --- a/database/migrations/2023_06_07_000001_create_pulse_tables.php +++ b/database/migrations/2023_06_07_000001_create_pulse_tables.php @@ -21,7 +21,7 @@ public function getConnection(): ?string */ public function up(): void { - $connection = $this->getConnection() ?? DB::connection(); + $connection = DB::connection($this->getConnection()); Schema::create('pulse_values', function (Blueprint $table) use ($connection) { $table->engine = 'InnoDB'; From 0683a7527f7cadfc14cfc68ea5f90895dae3a17c Mon Sep 17 00:00:00 2001 From: taylorotwell Date: Tue, 5 Dec 2023 22:27:37 +0000 Subject: [PATCH 032/110] Fix code styling --- src/Contracts/Storage.php | 8 ++++---- src/Pulse.php | 6 +++--- src/Storage/DatabaseStorage.php | 8 ++++---- src/Support/RedisAdapter.php | 2 +- tests/Pest.php | 2 +- tests/StorageFake.php | 8 ++++---- 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/Contracts/Storage.php b/src/Contracts/Storage.php index f1bc2966..504a9286 100644 --- a/src/Contracts/Storage.php +++ b/src/Contracts/Storage.php @@ -24,7 +24,7 @@ public function trim(): void; * * @param list $types */ - public function purge(array $types = null): void; + public function purge(?array $types = null): void; /** * Retrieve values for the given type. @@ -43,7 +43,7 @@ public function purge(array $types = null): void; * > * > */ - public function values(string $type, array $keys = null): Collection; + public function values(string $type, ?array $keys = null): Collection; /** * Retrieve aggregate values for plotting on a graph. @@ -69,7 +69,7 @@ public function aggregate( string $type, string|array $aggregates, CarbonInterval $interval, - string $orderBy = null, + ?string $orderBy = null, string $direction = 'desc', int $limit = 101, ): Collection; @@ -84,7 +84,7 @@ public function aggregateTypes( string|array $types, string $aggregate, CarbonInterval $interval, - string $orderBy = null, + ?string $orderBy = null, string $direction = 'desc', int $limit = 101, ): Collection; diff --git a/src/Pulse.php b/src/Pulse.php index 961dc134..ab9f5716 100644 --- a/src/Pulse.php +++ b/src/Pulse.php @@ -149,8 +149,8 @@ public function register(array $recorders): self public function record( string $type, string $key, - int $value = null, - DateTimeInterface|int $timestamp = null, + ?int $value = null, + DateTimeInterface|int|null $timestamp = null, ): Entry { if ($timestamp === null) { $timestamp = CarbonImmutable::now(); @@ -177,7 +177,7 @@ public function set( string $type, string $key, string $value, - DateTimeInterface|int $timestamp = null, + DateTimeInterface|int|null $timestamp = null, ): Value { if ($timestamp === null) { $timestamp = CarbonImmutable::now(); diff --git a/src/Storage/DatabaseStorage.php b/src/Storage/DatabaseStorage.php index 66c12fcc..2c230243 100644 --- a/src/Storage/DatabaseStorage.php +++ b/src/Storage/DatabaseStorage.php @@ -120,7 +120,7 @@ public function trim(): void * * @param list $types */ - public function purge(array $types = null): void + public function purge(?array $types = null): void { if ($types === null) { $this->connection()->table('pulse_values')->truncate(); @@ -350,7 +350,7 @@ protected function periods(): array * > * > */ - public function values(string $type, array $keys = null): Collection + public function values(string $type, ?array $keys = null): Collection { return $this->connection() ->table('pulse_values') @@ -417,7 +417,7 @@ public function aggregate( string $type, array|string $aggregates, CarbonInterval $interval, - string $orderBy = null, + ?string $orderBy = null, string $direction = 'desc', int $limit = 101, ): Collection { @@ -520,7 +520,7 @@ public function aggregateTypes( string|array $types, string $aggregate, CarbonInterval $interval, - string $orderBy = null, + ?string $orderBy = null, string $direction = 'desc', int $limit = 101, ): Collection { diff --git a/src/Support/RedisAdapter.php b/src/Support/RedisAdapter.php index 9a773238..25d22495 100644 --- a/src/Support/RedisAdapter.php +++ b/src/Support/RedisAdapter.php @@ -47,7 +47,7 @@ public function xadd(string $key, array $dictionary): string|Pipeline|PhpRedis * * @return array> */ - public function xrange(string $key, string $start, string $end, int $count = null): array + public function xrange(string $key, string $start, string $end, ?int $count = null): array { return collect($this->handle([ // @phpstan-ignore return.type argument.templateType argument.templateType 'XRANGE', diff --git a/tests/Pest.php b/tests/Pest.php index 0694db1a..98421b73 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -54,7 +54,7 @@ | */ -expect()->extend('toContainAggregateForAllPeriods', function (string|array $type, string $aggregate, string $key, int $value, int $count = null, int $timestamp = null) { +expect()->extend('toContainAggregateForAllPeriods', function (string|array $type, string $aggregate, string $key, int $value, ?int $count = null, ?int $timestamp = null) { $this->toBeInstanceOf(Collection::class); $values = $this->value->each(function (stdClass $value) { diff --git a/tests/StorageFake.php b/tests/StorageFake.php index 4535b0f4..c9c8684a 100644 --- a/tests/StorageFake.php +++ b/tests/StorageFake.php @@ -39,7 +39,7 @@ public function trim(): void * * @param list $types */ - public function purge(array $types = null): void + public function purge(?array $types = null): void { // } @@ -61,7 +61,7 @@ public function purge(array $types = null): void * > * > */ - public function values(string $type, array $keys = null): Collection + public function values(string $type, ?array $keys = null): Collection { return new Collection(); } @@ -93,7 +93,7 @@ public function aggregate( string $type, array|string $aggregates, CarbonInterval $interval, - string $orderBy = null, + ?string $orderBy = null, string $direction = 'desc', int $limit = 101, ): Collection { @@ -110,7 +110,7 @@ public function aggregateTypes( string|array $types, string $aggregate, CarbonInterval $interval, - string $orderBy = null, + ?string $orderBy = null, string $direction = 'desc', int $limit = 101, ): Collection { From e65fbbe3b260f9cb3e8593dd89bb1631ee1a4c4d Mon Sep 17 00:00:00 2001 From: Tobias Oitzinger <42447585+toitzi@users.noreply.github.com> Date: Tue, 5 Dec 2023 23:46:13 +0100 Subject: [PATCH 033/110] [1.x] Make migrations publishable (#81) * Make migrations publishable Signed-off-by: Tobias Oitzinger * Remove implicit migration helpers * Remove migration methods --------- Signed-off-by: Tobias Oitzinger Co-authored-by: Tim MacDonald --- src/Facades/Pulse.php | 2 -- src/Pulse.php | 23 ----------------------- src/PulseServiceProvider.php | 18 ++++-------------- tests/TestCase.php | 1 + 4 files changed, 5 insertions(+), 39 deletions(-) diff --git a/src/Facades/Pulse.php b/src/Facades/Pulse.php index 1393f1ee..6471aaa4 100644 --- a/src/Facades/Pulse.php +++ b/src/Facades/Pulse.php @@ -26,8 +26,6 @@ * @method static \Laravel\Pulse\Pulse rememberUser(\Illuminate\Contracts\Auth\Authenticatable $user) * @method static string css() * @method static string js() - * @method static bool runsMigrations() - * @method static \Laravel\Pulse\Pulse ignoreMigrations() * @method static bool registersRoutes() * @method static \Laravel\Pulse\Pulse ignoreRoutes() * @method static \Laravel\Pulse\Pulse handleExceptionsUsing(callable $callback) diff --git a/src/Pulse.php b/src/Pulse.php index ab9f5716..696bb721 100644 --- a/src/Pulse.php +++ b/src/Pulse.php @@ -77,11 +77,6 @@ class Pulse */ protected int|string|null $rememberedUserId = null; - /** - * Indicates if Pulse migrations will be run. - */ - protected bool $runsMigrations = true; - /** * Indicates if Pulse routes will be registered. */ @@ -456,24 +451,6 @@ public function js(): string return $content; } - /** - * Determine if Pulse may run migrations. - */ - public function runsMigrations(): bool - { - return $this->runsMigrations; - } - - /** - * Configure Pulse to not register its migrations. - */ - public function ignoreMigrations(): self - { - $this->runsMigrations = false; - - return $this; - } - /** * Determine if Pulse may register routes. */ diff --git a/src/PulseServiceProvider.php b/src/PulseServiceProvider.php index 17e15089..fc3269a8 100644 --- a/src/PulseServiceProvider.php +++ b/src/PulseServiceProvider.php @@ -8,7 +8,6 @@ use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Contracts\Foundation\Application; use Illuminate\Contracts\Http\Kernel as HttpKernel; -use Illuminate\Database\Migrations\Migrator; use Illuminate\Queue\Events\Looping; use Illuminate\Queue\Events\WorkerStopping; use Illuminate\Routing\Router; @@ -75,7 +74,6 @@ public function boot(): void $this->listenForEvents(); $this->registerComponents(); $this->registerResources(); - $this->registerMigrations(); $this->registerPublishing(); $this->registerCommands(); } @@ -190,18 +188,6 @@ protected function registerResources(): void $this->loadViewsFrom(__DIR__.'/../resources/views', 'pulse'); } - /** - * Register the package's migrations. - */ - protected function registerMigrations(): void - { - $this->callAfterResolving('migrator', function (Migrator $migrator, Application $app) { - if ($app->make(Pulse::class)->runsMigrations()) { - $migrator->path(__DIR__.'/../database/migrations'); - } - }); - } - /** * Register the package's publishable resources. */ @@ -215,6 +201,10 @@ protected function registerPublishing(): void $this->publishes([ __DIR__.'/../resources/views/dashboard.blade.php' => resource_path('views/vendor/pulse/dashboard.blade.php'), ], ['pulse', 'pulse-dashboard']); + + $this->publishes([ + __DIR__.'/../database/migrations' => database_path('migrations'), + ], ['pulse', 'pulse-migrations']); } } diff --git a/tests/TestCase.php b/tests/TestCase.php index 08867bda..4dfb0310 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -21,6 +21,7 @@ protected function getPackageProviders($app): array protected function defineDatabaseMigrations(): void { + $this->loadMigrationsFrom(__DIR__.'/../database/migrations'); $this->loadMigrationsFrom(__DIR__.'/migrations'); } From 2487643e9617bd147653d67aab40f7865189fed6 Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Wed, 6 Dec 2023 09:46:32 +1100 Subject: [PATCH 034/110] Remove the engine specification (#153) --- database/migrations/2023_06_07_000001_create_pulse_tables.php | 3 --- 1 file changed, 3 deletions(-) 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 9f06312e..41f8520c 100644 --- a/database/migrations/2023_06_07_000001_create_pulse_tables.php +++ b/database/migrations/2023_06_07_000001_create_pulse_tables.php @@ -24,7 +24,6 @@ public function up(): void $connection = DB::connection($this->getConnection()); Schema::create('pulse_values', function (Blueprint $table) use ($connection) { - $table->engine = 'InnoDB'; $table->id(); $table->unsignedInteger('timestamp'); $table->string('type'); @@ -42,7 +41,6 @@ public function up(): void }); Schema::create('pulse_entries', function (Blueprint $table) use ($connection) { - $table->engine = 'InnoDB'; $table->id(); $table->unsignedInteger('timestamp'); $table->string('type'); @@ -61,7 +59,6 @@ public function up(): void }); Schema::create('pulse_aggregates', function (Blueprint $table) use ($connection) { - $table->engine = 'InnoDB'; $table->id(); $table->unsignedInteger('bucket'); $table->unsignedMediumInteger('period'); From 27055078f4cf3ad771e4fa23ec6da50a170c5d1d Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Wed, 6 Dec 2023 09:49:00 +1100 Subject: [PATCH 035/110] Remove default `web` middleware (#152) --- src/PulseServiceProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PulseServiceProvider.php b/src/PulseServiceProvider.php index fc3269a8..172d365d 100644 --- a/src/PulseServiceProvider.php +++ b/src/PulseServiceProvider.php @@ -98,7 +98,7 @@ protected function registerRoutes(): void $router->group([ 'domain' => $app->make('config')->get('pulse.domain', null), 'prefix' => $app->make('config')->get('pulse.path'), - 'middleware' => $app->make('config')->get('pulse.middleware', 'web'), + 'middleware' => $app->make('config')->get('pulse.middleware'), ], function (Router $router) { $router->get('/', function (Pulse $pulse, ViewFactory $view) { return $view->make('pulse::dashboard'); From e08fef3f5e5c9be9b04ecf240148facd89145a55 Mon Sep 17 00:00:00 2001 From: Jess Archer Date: Wed, 6 Dec 2023 09:21:01 +1000 Subject: [PATCH 036/110] Pint fixes --- src/Contracts/Storage.php | 8 ++++---- src/Pulse.php | 6 +++--- src/Storage/DatabaseStorage.php | 8 ++++---- src/Support/RedisAdapter.php | 2 +- tests/Pest.php | 2 +- tests/StorageFake.php | 8 ++++---- 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/Contracts/Storage.php b/src/Contracts/Storage.php index 504a9286..f1bc2966 100644 --- a/src/Contracts/Storage.php +++ b/src/Contracts/Storage.php @@ -24,7 +24,7 @@ public function trim(): void; * * @param list $types */ - public function purge(?array $types = null): void; + public function purge(array $types = null): void; /** * Retrieve values for the given type. @@ -43,7 +43,7 @@ public function purge(?array $types = null): void; * > * > */ - public function values(string $type, ?array $keys = null): Collection; + public function values(string $type, array $keys = null): Collection; /** * Retrieve aggregate values for plotting on a graph. @@ -69,7 +69,7 @@ public function aggregate( string $type, string|array $aggregates, CarbonInterval $interval, - ?string $orderBy = null, + string $orderBy = null, string $direction = 'desc', int $limit = 101, ): Collection; @@ -84,7 +84,7 @@ public function aggregateTypes( string|array $types, string $aggregate, CarbonInterval $interval, - ?string $orderBy = null, + string $orderBy = null, string $direction = 'desc', int $limit = 101, ): Collection; diff --git a/src/Pulse.php b/src/Pulse.php index 696bb721..8cbb3cae 100644 --- a/src/Pulse.php +++ b/src/Pulse.php @@ -144,8 +144,8 @@ public function register(array $recorders): self public function record( string $type, string $key, - ?int $value = null, - DateTimeInterface|int|null $timestamp = null, + int $value = null, + DateTimeInterface|int $timestamp = null, ): Entry { if ($timestamp === null) { $timestamp = CarbonImmutable::now(); @@ -172,7 +172,7 @@ public function set( string $type, string $key, string $value, - DateTimeInterface|int|null $timestamp = null, + DateTimeInterface|int $timestamp = null, ): Value { if ($timestamp === null) { $timestamp = CarbonImmutable::now(); diff --git a/src/Storage/DatabaseStorage.php b/src/Storage/DatabaseStorage.php index 2c230243..66c12fcc 100644 --- a/src/Storage/DatabaseStorage.php +++ b/src/Storage/DatabaseStorage.php @@ -120,7 +120,7 @@ public function trim(): void * * @param list $types */ - public function purge(?array $types = null): void + public function purge(array $types = null): void { if ($types === null) { $this->connection()->table('pulse_values')->truncate(); @@ -350,7 +350,7 @@ protected function periods(): array * > * > */ - public function values(string $type, ?array $keys = null): Collection + public function values(string $type, array $keys = null): Collection { return $this->connection() ->table('pulse_values') @@ -417,7 +417,7 @@ public function aggregate( string $type, array|string $aggregates, CarbonInterval $interval, - ?string $orderBy = null, + string $orderBy = null, string $direction = 'desc', int $limit = 101, ): Collection { @@ -520,7 +520,7 @@ public function aggregateTypes( string|array $types, string $aggregate, CarbonInterval $interval, - ?string $orderBy = null, + string $orderBy = null, string $direction = 'desc', int $limit = 101, ): Collection { diff --git a/src/Support/RedisAdapter.php b/src/Support/RedisAdapter.php index 25d22495..9a773238 100644 --- a/src/Support/RedisAdapter.php +++ b/src/Support/RedisAdapter.php @@ -47,7 +47,7 @@ public function xadd(string $key, array $dictionary): string|Pipeline|PhpRedis * * @return array> */ - public function xrange(string $key, string $start, string $end, ?int $count = null): array + public function xrange(string $key, string $start, string $end, int $count = null): array { return collect($this->handle([ // @phpstan-ignore return.type argument.templateType argument.templateType 'XRANGE', diff --git a/tests/Pest.php b/tests/Pest.php index 98421b73..0694db1a 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -54,7 +54,7 @@ | */ -expect()->extend('toContainAggregateForAllPeriods', function (string|array $type, string $aggregate, string $key, int $value, ?int $count = null, ?int $timestamp = null) { +expect()->extend('toContainAggregateForAllPeriods', function (string|array $type, string $aggregate, string $key, int $value, int $count = null, int $timestamp = null) { $this->toBeInstanceOf(Collection::class); $values = $this->value->each(function (stdClass $value) { diff --git a/tests/StorageFake.php b/tests/StorageFake.php index c9c8684a..4535b0f4 100644 --- a/tests/StorageFake.php +++ b/tests/StorageFake.php @@ -39,7 +39,7 @@ public function trim(): void * * @param list $types */ - public function purge(?array $types = null): void + public function purge(array $types = null): void { // } @@ -61,7 +61,7 @@ public function purge(?array $types = null): void * > * > */ - public function values(string $type, ?array $keys = null): Collection + public function values(string $type, array $keys = null): Collection { return new Collection(); } @@ -93,7 +93,7 @@ public function aggregate( string $type, array|string $aggregates, CarbonInterval $interval, - ?string $orderBy = null, + string $orderBy = null, string $direction = 'desc', int $limit = 101, ): Collection { @@ -110,7 +110,7 @@ public function aggregateTypes( string|array $types, string $aggregate, CarbonInterval $interval, - ?string $orderBy = null, + string $orderBy = null, string $direction = 'desc', int $limit = 101, ): Collection { From b1cbeca8c3ca4b22f9f550d877136892bdcea741 Mon Sep 17 00:00:00 2001 From: jessarcher Date: Tue, 5 Dec 2023 23:21:34 +0000 Subject: [PATCH 037/110] Fix code styling --- src/Contracts/Storage.php | 8 ++++---- src/Pulse.php | 6 +++--- src/Storage/DatabaseStorage.php | 8 ++++---- src/Support/RedisAdapter.php | 2 +- tests/Pest.php | 2 +- tests/StorageFake.php | 8 ++++---- 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/Contracts/Storage.php b/src/Contracts/Storage.php index f1bc2966..504a9286 100644 --- a/src/Contracts/Storage.php +++ b/src/Contracts/Storage.php @@ -24,7 +24,7 @@ public function trim(): void; * * @param list $types */ - public function purge(array $types = null): void; + public function purge(?array $types = null): void; /** * Retrieve values for the given type. @@ -43,7 +43,7 @@ public function purge(array $types = null): void; * > * > */ - public function values(string $type, array $keys = null): Collection; + public function values(string $type, ?array $keys = null): Collection; /** * Retrieve aggregate values for plotting on a graph. @@ -69,7 +69,7 @@ public function aggregate( string $type, string|array $aggregates, CarbonInterval $interval, - string $orderBy = null, + ?string $orderBy = null, string $direction = 'desc', int $limit = 101, ): Collection; @@ -84,7 +84,7 @@ public function aggregateTypes( string|array $types, string $aggregate, CarbonInterval $interval, - string $orderBy = null, + ?string $orderBy = null, string $direction = 'desc', int $limit = 101, ): Collection; diff --git a/src/Pulse.php b/src/Pulse.php index 8cbb3cae..696bb721 100644 --- a/src/Pulse.php +++ b/src/Pulse.php @@ -144,8 +144,8 @@ public function register(array $recorders): self public function record( string $type, string $key, - int $value = null, - DateTimeInterface|int $timestamp = null, + ?int $value = null, + DateTimeInterface|int|null $timestamp = null, ): Entry { if ($timestamp === null) { $timestamp = CarbonImmutable::now(); @@ -172,7 +172,7 @@ public function set( string $type, string $key, string $value, - DateTimeInterface|int $timestamp = null, + DateTimeInterface|int|null $timestamp = null, ): Value { if ($timestamp === null) { $timestamp = CarbonImmutable::now(); diff --git a/src/Storage/DatabaseStorage.php b/src/Storage/DatabaseStorage.php index 66c12fcc..2c230243 100644 --- a/src/Storage/DatabaseStorage.php +++ b/src/Storage/DatabaseStorage.php @@ -120,7 +120,7 @@ public function trim(): void * * @param list $types */ - public function purge(array $types = null): void + public function purge(?array $types = null): void { if ($types === null) { $this->connection()->table('pulse_values')->truncate(); @@ -350,7 +350,7 @@ protected function periods(): array * > * > */ - public function values(string $type, array $keys = null): Collection + public function values(string $type, ?array $keys = null): Collection { return $this->connection() ->table('pulse_values') @@ -417,7 +417,7 @@ public function aggregate( string $type, array|string $aggregates, CarbonInterval $interval, - string $orderBy = null, + ?string $orderBy = null, string $direction = 'desc', int $limit = 101, ): Collection { @@ -520,7 +520,7 @@ public function aggregateTypes( string|array $types, string $aggregate, CarbonInterval $interval, - string $orderBy = null, + ?string $orderBy = null, string $direction = 'desc', int $limit = 101, ): Collection { diff --git a/src/Support/RedisAdapter.php b/src/Support/RedisAdapter.php index 9a773238..25d22495 100644 --- a/src/Support/RedisAdapter.php +++ b/src/Support/RedisAdapter.php @@ -47,7 +47,7 @@ public function xadd(string $key, array $dictionary): string|Pipeline|PhpRedis * * @return array> */ - public function xrange(string $key, string $start, string $end, int $count = null): array + public function xrange(string $key, string $start, string $end, ?int $count = null): array { return collect($this->handle([ // @phpstan-ignore return.type argument.templateType argument.templateType 'XRANGE', diff --git a/tests/Pest.php b/tests/Pest.php index 0694db1a..98421b73 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -54,7 +54,7 @@ | */ -expect()->extend('toContainAggregateForAllPeriods', function (string|array $type, string $aggregate, string $key, int $value, int $count = null, int $timestamp = null) { +expect()->extend('toContainAggregateForAllPeriods', function (string|array $type, string $aggregate, string $key, int $value, ?int $count = null, ?int $timestamp = null) { $this->toBeInstanceOf(Collection::class); $values = $this->value->each(function (stdClass $value) { diff --git a/tests/StorageFake.php b/tests/StorageFake.php index 4535b0f4..c9c8684a 100644 --- a/tests/StorageFake.php +++ b/tests/StorageFake.php @@ -39,7 +39,7 @@ public function trim(): void * * @param list $types */ - public function purge(array $types = null): void + public function purge(?array $types = null): void { // } @@ -61,7 +61,7 @@ public function purge(array $types = null): void * > * > */ - public function values(string $type, array $keys = null): Collection + public function values(string $type, ?array $keys = null): Collection { return new Collection(); } @@ -93,7 +93,7 @@ public function aggregate( string $type, array|string $aggregates, CarbonInterval $interval, - string $orderBy = null, + ?string $orderBy = null, string $direction = 'desc', int $limit = 101, ): Collection { @@ -110,7 +110,7 @@ public function aggregateTypes( string|array $types, string $aggregate, CarbonInterval $interval, - string $orderBy = null, + ?string $orderBy = null, string $direction = 'desc', int $limit = 101, ): Collection { From a7e410fb65d827397db2aa25e67b8d2a0c0a7ed0 Mon Sep 17 00:00:00 2001 From: Jess Archer Date: Wed, 6 Dec 2023 09:22:22 +1000 Subject: [PATCH 038/110] Fix layout issues (#155) * Fix layout issues * Fix code styling --------- Co-authored-by: jessarcher --- resources/views/livewire/cache.blade.php | 4 ++-- resources/views/livewire/exceptions.blade.php | 4 ++-- resources/views/livewire/queues.blade.php | 2 +- resources/views/livewire/servers.blade.php | 16 ++++++++-------- resources/views/livewire/slow-jobs.blade.php | 4 ++-- .../livewire/slow-outgoing-requests.blade.php | 4 ++-- resources/views/livewire/slow-queries.blade.php | 4 ++-- resources/views/livewire/slow-requests.blade.php | 4 ++-- resources/views/livewire/usage.blade.php | 2 +- 9 files changed, 22 insertions(+), 22 deletions(-) diff --git a/resources/views/livewire/cache.blade.php b/resources/views/livewire/cache.blade.php index ed296564..fcbce41d 100644 --- a/resources/views/livewire/cache.blade.php +++ b/resources/views/livewire/cache.blade.php @@ -83,8 +83,8 @@ @foreach ($cacheKeyInteractions->take(100) as $interaction) - - + + {{ $interaction->key }} diff --git a/resources/views/livewire/exceptions.blade.php b/resources/views/livewire/exceptions.blade.php index 9874b2fb..7265c1cd 100644 --- a/resources/views/livewire/exceptions.blade.php +++ b/resources/views/livewire/exceptions.blade.php @@ -40,8 +40,8 @@ @foreach ($exceptions->take(100) as $exception) - - + + {{ $exception->class }} diff --git a/resources/views/livewire/queues.blade.php b/resources/views/livewire/queues.blade.php index 5c823193..00357645 100644 --- a/resources/views/livewire/queues.blade.php +++ b/resources/views/livewire/queues.blade.php @@ -42,7 +42,7 @@ @else
@foreach ($queues as $queue => $readings) -
+

@if ($showConnection) {{ $queue }} diff --git a/resources/views/livewire/servers.blade.php b/resources/views/livewire/servers.blade.php index 9b15b774..18595ad0 100644 --- a/resources/views/livewire/servers.blade.php +++ b/resources/views/livewire/servers.blade.php @@ -38,7 +38,7 @@ class="overflow-x-auto pb-px default:col-span-full default:lg:col-span-{{ $cols
Storage
@foreach ($servers as $slug => $server) -
+
@if ($server->recently_reported)
@@ -47,16 +47,16 @@ class="overflow-x-auto pb-px default:col-span-full default:lg:col-span-{{ $cols @endif
-
+
{{ $server->name }}
-
+
{{ $server->cpu_current }}%
-
+
-
+
{{ $friendlySize($server->memory_current, 1) }} @@ -153,7 +153,7 @@ class="w-full min-w-[5rem] max-w-xs h-9 relative"
-
+
-
+
@foreach ($server->storage as $storage) -
+
{{ $friendlySize($storage->used) }} / {{ $friendlySize($storage->total) }} diff --git a/resources/views/livewire/slow-jobs.blade.php b/resources/views/livewire/slow-jobs.blade.php index 8fe0357e..a84cb65c 100644 --- a/resources/views/livewire/slow-jobs.blade.php +++ b/resources/views/livewire/slow-jobs.blade.php @@ -39,8 +39,8 @@ @foreach ($slowJobs->take(100) as $job) - - + + {{ $job->job }} diff --git a/resources/views/livewire/slow-outgoing-requests.blade.php b/resources/views/livewire/slow-outgoing-requests.blade.php index 9fde1ddb..f2d737e9 100644 --- a/resources/views/livewire/slow-outgoing-requests.blade.php +++ b/resources/views/livewire/slow-outgoing-requests.blade.php @@ -57,8 +57,8 @@ @foreach ($slowOutgoingRequests->take(100) as $request) - - + + diff --git a/resources/views/livewire/slow-queries.blade.php b/resources/views/livewire/slow-queries.blade.php index bbb5da3f..299db5e2 100644 --- a/resources/views/livewire/slow-queries.blade.php +++ b/resources/views/livewire/slow-queries.blade.php @@ -55,8 +55,8 @@ @foreach ($slowQueries->take(100) as $query) - - + +
diff --git a/resources/views/livewire/slow-requests.blade.php b/resources/views/livewire/slow-requests.blade.php index a69a87d4..529a5d71 100644 --- a/resources/views/livewire/slow-requests.blade.php +++ b/resources/views/livewire/slow-requests.blade.php @@ -41,8 +41,8 @@ @foreach ($slowRequests->take(100) as $slowRequest) - - + + diff --git a/resources/views/livewire/usage.blade.php b/resources/views/livewire/usage.blade.php index 2e98a999..e3e1b054 100644 --- a/resources/views/livewire/usage.blade.php +++ b/resources/views/livewire/usage.blade.php @@ -40,7 +40,7 @@ class="flex-1" @else
@foreach ($userRequestCounts as $userRequestCount) - + @if ($userRequestCount->user->avatar ?? false) From 27d5ce977fd17aae1658e3c6dd716e6bd75c37d1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Dec 2023 17:22:36 -0600 Subject: [PATCH 039/110] [1.x] Bump postcss from 8.4.29 to 8.4.31 (#154) * Bump postcss from 8.4.29 to 8.4.31 Bumps [postcss](https://github.com/postcss/postcss) from 8.4.29 to 8.4.31. - [Release notes](https://github.com/postcss/postcss/releases) - [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/postcss/postcss/compare/8.4.29...8.4.31) --- updated-dependencies: - dependency-name: postcss dependency-type: direct:development ... Signed-off-by: dependabot[bot] * Update build --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Tim MacDonald --- dist/pulse.css | 2 +- package-lock.json | 8 ++++---- package.json | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dist/pulse.css b/dist/pulse.css index b53906d1..baa3afc4 100644 --- a/dist/pulse.css +++ b/dist/pulse.css @@ -1 +1 @@ -*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Figtree,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}[type=text],input:where(:not([type])),[type=email],[type=url],[type=password],[type=number],[type=date],[type=datetime-local],[type=month],[type=search],[type=tel],[type=time],[type=week],[multiple],textarea,select{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#6b7280;border-width:1px;border-radius:0;padding:.5rem .75rem;font-size:1rem;line-height:1.5rem;--tw-shadow: 0 0 #0000}[type=text]:focus,input:where(:not([type])):focus,[type=email]:focus,[type=url]:focus,[type=password]:focus,[type=number]:focus,[type=date]:focus,[type=datetime-local]:focus,[type=month]:focus,[type=search]:focus,[type=tel]:focus,[type=time]:focus,[type=week]:focus,[multiple]:focus,textarea:focus,select:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-inset: var(--tw-empty, );--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: #2563eb;--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);border-color:#2563eb}input::-moz-placeholder,textarea::-moz-placeholder{color:#6b7280;opacity:1}input::placeholder,textarea::placeholder{color:#6b7280;opacity:1}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-date-and-time-value{min-height:1.5em;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit,::-webkit-datetime-edit-year-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-meridiem-field{padding-top:0;padding-bottom:0}select{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;padding-right:2.5rem;-webkit-print-color-adjust:exact;print-color-adjust:exact}[multiple],[size]:where(select:not([size="1"])){background-image:initial;background-position:initial;background-repeat:unset;background-size:initial;padding-right:.75rem;-webkit-print-color-adjust:unset;print-color-adjust:unset}[type=checkbox],[type=radio]{-webkit-appearance:none;-moz-appearance:none;appearance:none;padding:0;-webkit-print-color-adjust:exact;print-color-adjust:exact;display:inline-block;vertical-align:middle;background-origin:border-box;-webkit-user-select:none;-moz-user-select:none;user-select:none;flex-shrink:0;height:1rem;width:1rem;color:#2563eb;background-color:#fff;border-color:#6b7280;border-width:1px;--tw-shadow: 0 0 #0000}[type=checkbox]{border-radius:0}[type=radio]{border-radius:100%}[type=checkbox]:focus,[type=radio]:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-inset: var(--tw-empty, );--tw-ring-offset-width: 2px;--tw-ring-offset-color: #fff;--tw-ring-color: #2563eb;--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}[type=checkbox]:checked,[type=radio]:checked{border-color:transparent;background-color:currentColor;background-size:100% 100%;background-position:center;background-repeat:no-repeat}[type=checkbox]:checked{background-image:url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e")}[type=radio]:checked{background-image:url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e")}[type=checkbox]:checked:hover,[type=checkbox]:checked:focus,[type=radio]:checked:hover,[type=radio]:checked:focus{border-color:transparent;background-color:currentColor}[type=checkbox]:indeterminate{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3e%3c/svg%3e");border-color:transparent;background-color:currentColor;background-size:100% 100%;background-position:center;background-repeat:no-repeat}[type=checkbox]:indeterminate:hover,[type=checkbox]:indeterminate:focus{border-color:transparent;background-color:currentColor}[type=file]{background:unset;border-color:inherit;border-width:0;border-radius:0;padding:0;font-size:unset;line-height:inherit}[type=file]:focus{outline:1px solid ButtonText;outline:1px auto -webkit-focus-ring-color}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.container{width:100%}@media (min-width: 640px){.container{max-width:640px}}@media (min-width: 768px){.container{max-width:768px}}@media (min-width: 1024px){.container{max-width:1024px}}@media (min-width: 1280px){.container{max-width:1280px}}@media (min-width: 1536px){.container{max-width:1536px}}.pointer-events-none{pointer-events:none}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.-left-px{left:-1px}.-top-2{top:-.5rem}.bottom-0{bottom:0}.left-0{left:0}.right-0{right:0}.top-0{top:0}.z-10{z-index:10}.col-span-1{grid-column:span 1 / span 1}.col-span-10{grid-column:span 10 / span 10}.col-span-11{grid-column:span 11 / span 11}.col-span-12{grid-column:span 12 / span 12}.col-span-2{grid-column:span 2 / span 2}.col-span-3{grid-column:span 3 / span 3}.col-span-4{grid-column:span 4 / span 4}.col-span-5{grid-column:span 5 / span 5}.col-span-6{grid-column:span 6 / span 6}.col-span-7{grid-column:span 7 / span 7}.col-span-8{grid-column:span 8 / span 8}.col-span-9{grid-column:span 9 / span 9}.col-span-full{grid-column:1 / -1}.row-span-1{grid-row:span 1 / span 1}.row-span-2{grid-row:span 2 / span 2}.row-span-3{grid-row:span 3 / span 3}.row-span-4{grid-row:span 4 / span 4}.row-span-5{grid-row:span 5 / span 5}.row-span-6{grid-row:span 6 / span 6}.row-span-full{grid-row:1 / -1}.mx-auto{margin-left:auto;margin-right:auto}.mx-px{margin-left:1px;margin-right:1px}.mb-3{margin-bottom:.75rem}.mb-px{margin-bottom:1px}.ml-2{margin-left:.5rem}.mr-1{margin-right:.25rem}.mr-2{margin-right:.5rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.block{display:block}.flex{display:flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-0{height:0px}.h-0\.5{height:.125rem}.h-1{height:.25rem}.h-1\.5{height:.375rem}.h-1\/2{height:50%}.h-1\/3{height:33.333333%}.h-1\/4{height:25%}.h-1\/5{height:20%}.h-1\/6{height:16.666667%}.h-10{height:2.5rem}.h-11{height:2.75rem}.h-12{height:3rem}.h-128{height:32rem}.h-14{height:3.5rem}.h-16{height:4rem}.h-2{height:.5rem}.h-2\.5{height:.625rem}.h-2\/3{height:66.666667%}.h-2\/4{height:50%}.h-2\/5{height:40%}.h-2\/6{height:33.333333%}.h-20{height:5rem}.h-24{height:6rem}.h-28{height:7rem}.h-3{height:.75rem}.h-3\.5{height:.875rem}.h-3\/4{height:75%}.h-3\/5{height:60%}.h-3\/6{height:50%}.h-32{height:8rem}.h-36{height:9rem}.h-4{height:1rem}.h-4\/5{height:80%}.h-4\/6{height:66.666667%}.h-40{height:10rem}.h-44{height:11rem}.h-48{height:12rem}.h-5{height:1.25rem}.h-5\/6{height:83.333333%}.h-52{height:13rem}.h-56{height:14rem}.h-6{height:1.5rem}.h-60{height:15rem}.h-64{height:16rem}.h-7{height:1.75rem}.h-72{height:18rem}.h-8{height:2rem}.h-80{height:20rem}.h-9{height:2.25rem}.h-96{height:24rem}.h-\[30px\]{height:30px}.h-\[52px\]{height:52px}.h-full{height:100%}.max-h-0{max-height:0px}.max-h-0\.5{max-height:.125rem}.max-h-1{max-height:.25rem}.max-h-1\.5{max-height:.375rem}.max-h-10{max-height:2.5rem}.max-h-11{max-height:2.75rem}.max-h-12{max-height:3rem}.max-h-14{max-height:3.5rem}.max-h-16{max-height:4rem}.max-h-2{max-height:.5rem}.max-h-2\.5{max-height:.625rem}.max-h-20{max-height:5rem}.max-h-24{max-height:6rem}.max-h-28{max-height:7rem}.max-h-3{max-height:.75rem}.max-h-3\.5{max-height:.875rem}.max-h-32{max-height:8rem}.max-h-36{max-height:9rem}.max-h-4{max-height:1rem}.max-h-40{max-height:10rem}.max-h-44{max-height:11rem}.max-h-48{max-height:12rem}.max-h-5{max-height:1.25rem}.max-h-52{max-height:13rem}.max-h-56{max-height:14rem}.max-h-6{max-height:1.5rem}.max-h-60{max-height:15rem}.max-h-64{max-height:16rem}.max-h-7{max-height:1.75rem}.max-h-72{max-height:18rem}.max-h-8{max-height:2rem}.max-h-80{max-height:20rem}.max-h-9{max-height:2.25rem}.max-h-96{max-height:24rem}.min-h-0{min-height:0px}.min-h-full{min-height:100%}.min-h-screen{min-height:100vh}.w-1{width:.25rem}.w-1\/12{width:8.333333%}.w-1\/2{width:50%}.w-14{width:3.5rem}.w-2\/12{width:16.666667%}.w-3{width:.75rem}.w-3\/12{width:25%}.w-36{width:9rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-8{width:2rem}.w-full{width:100%}.min-w-\[5rem\]{min-width:5rem}.max-w-\[1px\]{max-width:1px}.max-w-fit{max-width:-moz-fit-content;max-width:fit-content}.max-w-full{max-width:100%}.max-w-xs{max-width:20rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.flex-grow{flex-grow:1}.flex-grow-\[10000\]{flex-grow:10000}.basis-0{flex-basis:0px}.basis-56{flex-basis:14rem}.basis-full{flex-basis:100%}.origin-bottom{transform-origin:bottom}.origin-top-right{transform-origin:top right}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-10{grid-template-columns:repeat(10,minmax(0,1fr))}.grid-cols-11{grid-template-columns:repeat(11,minmax(0,1fr))}.grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.grid-cols-7{grid-template-columns:repeat(7,minmax(0,1fr))}.grid-cols-8{grid-template-columns:repeat(8,minmax(0,1fr))}.grid-cols-9{grid-template-columns:repeat(9,minmax(0,1fr))}.grid-cols-\[max-content\,minmax\(max-content\,1fr\)\,max-content\,minmax\(min-content\,2fr\)\,max-content\,minmax\(min-content\,2fr\)\,minmax\(max-content\,1fr\)\]{grid-template-columns:max-content minmax(max-content,1fr) max-content minmax(min-content,2fr) max-content minmax(min-content,2fr) minmax(max-content,1fr)}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.items-baseline{align-items:baseline}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.gap-8{gap:2rem}.gap-x-2{-moz-column-gap:.5rem;column-gap:.5rem}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.overflow-ellipsis{text-overflow:ellipsis}.whitespace-nowrap{white-space:nowrap}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.rounded-r-md{border-top-right-radius:.375rem;border-bottom-right-radius:.375rem}.border{border-width:1px}.border-0{border-width:0px}.border-b{border-bottom-width:1px}.border-r{border-right-width:1px}.border-blue-200{--tw-border-opacity: 1;border-color:rgb(191 219 254 / var(--tw-border-opacity))}.border-gray-200{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity))}.border-purple-200{--tw-border-opacity: 1;border-color:rgb(233 213 255 / var(--tw-border-opacity))}.border-red-200{--tw-border-opacity: 1;border-color:rgb(254 202 202 / var(--tw-border-opacity))}.bg-\[\#9333ea\]{--tw-bg-opacity: 1;background-color:rgb(147 51 234 / var(--tw-bg-opacity))}.bg-\[\#e11d48\]{--tw-bg-opacity: 1;background-color:rgb(225 29 72 / var(--tw-bg-opacity))}.bg-\[\#eab308\]{--tw-bg-opacity: 1;background-color:rgb(234 179 8 / var(--tw-bg-opacity))}.bg-\[rgba\(107\,114\,128\,0\.5\)\]{background-color:#6b728080}.bg-\[rgba\(147\,51\,234\,0\.5\)\]{background-color:#9333ea80}.bg-blue-50{--tw-bg-opacity: 1;background-color:rgb(239 246 255 / var(--tw-bg-opacity))}.bg-gray-100{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity))}.bg-gray-700{--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity))}.bg-green-500{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity))}.bg-purple-50{--tw-bg-opacity: 1;background-color:rgb(250 245 255 / var(--tw-bg-opacity))}.bg-purple-500{--tw-bg-opacity: 1;background-color:rgb(168 85 247 / var(--tw-bg-opacity))}.bg-red-50{--tw-bg-opacity: 1;background-color:rgb(254 242 242 / var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity))}.bg-gradient-to-r{background-image:linear-gradient(to right,var(--tw-gradient-stops))}.bg-gradient-to-t{background-image:linear-gradient(to top,var(--tw-gradient-stops))}.from-transparent{--tw-gradient-from: transparent var(--tw-gradient-from-position);--tw-gradient-to: rgb(0 0 0 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.from-white{--tw-gradient-from: #fff var(--tw-gradient-from-position);--tw-gradient-to: rgb(255 255 255 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.to-gray-700{--tw-gradient-to: #374151 var(--tw-gradient-to-position)}.stroke-gray-300{stroke:#d1d5db}.stroke-gray-400{stroke:#9ca3af}.stroke-gray-500{stroke:#6b7280}.stroke-red-500{stroke:#ef4444}.\!p-0{padding:0!important}.p-1{padding:.25rem}.p-3{padding:.75rem}.p-4{padding:1rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.pb-12{padding-bottom:3rem}.pb-px{padding-bottom:1px}.pl-3{padding-left:.75rem}.pr-8{padding-right:2rem}.pt-6{padding-top:1.5rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.font-sans{font-family:Figtree,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji"}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.tabular-nums{--tw-numeric-spacing: tabular-nums;font-variant-numeric:var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction)}.leading-none{line-height:1}.text-blue-400{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity))}.text-cyan-200{--tw-text-opacity: 1;color:rgb(165 243 252 / var(--tw-text-opacity))}.text-gray-100{--tw-text-opacity: 1;color:rgb(243 244 246 / var(--tw-text-opacity))}.text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity))}.text-orange-200{--tw-text-opacity: 1;color:rgb(254 215 170 / var(--tw-text-opacity))}.text-purple-200{--tw-text-opacity: 1;color:rgb(233 213 255 / var(--tw-text-opacity))}.text-purple-400{--tw-text-opacity: 1;color:rgb(192 132 252 / var(--tw-text-opacity))}.text-red-200{--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.opacity-25{opacity:.25}.shadow-none{--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.ring-1{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-gray-900\/5{--tw-ring-color: rgb(17 24 39 / .05)}.\@container{container-type:inline-size}.\@container\/scroll-wrapper{container-type:inline-size;container-name:scroll-wrapper}.\[scrollbar-color\:theme\(colors\.gray\.500\)_transparent\]{scrollbar-color:#6b7280 transparent}.\[scrollbar-width\:thin\]{scrollbar-width:thin}[x-cloak]{display:none}.after\:absolute:after{content:var(--tw-content);position:absolute}.after\:right-\[calc\(-1\*var\(--triangle-size\)\)\]:after{content:var(--tw-content);right:calc(-1 * var(--triangle-size))}.after\:top-\[calc\(50\%-var\(--triangle-size\)\)\]:after{content:var(--tw-content);top:calc(50% - var(--triangle-size))}.after\:border-b-\[length\:var\(--triangle-size\)\]:after{content:var(--tw-content);border-bottom-width:var(--triangle-size)}.after\:border-l-\[length\:var\(--triangle-size\)\]:after{content:var(--tw-content);border-left-width:var(--triangle-size)}.after\:border-t-\[length\:var\(--triangle-size\)\]:after{content:var(--tw-content);border-top-width:var(--triangle-size)}.after\:border-transparent:after{content:var(--tw-content);border-color:transparent}.after\:border-l-purple-500:after{content:var(--tw-content);--tw-border-opacity: 1;border-left-color:rgb(168 85 247 / var(--tw-border-opacity))}.after\:\[--triangle-size\:4px\]:after{content:var(--tw-content);--triangle-size: 4px}.first\:h-0:first-child{height:0px}.first\:rounded-l-md:first-child{border-top-left-radius:.375rem;border-bottom-left-radius:.375rem}.first\:pl-3:first-child{padding-left:.75rem}.last\:rounded-r-md:last-child{border-top-right-radius:.375rem;border-bottom-right-radius:.375rem}.last\:pr-3:last-child{padding-right:.75rem}.focus-within\:ring:focus-within{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.hover\:bg-gray-100:hover{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity))}.hover\:text-gray-400:hover{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity))}.hover\:text-gray-500:hover{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}.focus\:text-gray-500:focus{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}.focus\:ring-0:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}@container (min-width: 24rem){.\@sm\:block{display:block}.\@sm\:px-3{padding-left:.75rem;padding-right:.75rem}}@container (min-width: 28rem){.\@md\:mb-6{margin-bottom:1.5rem}}@container (min-width: 32rem){.\@lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@container (min-width: 48rem){.\@3xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}@container (min-width: 72rem){.\@6xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}html :where(.default\:col-span-1){grid-column:span 1 / span 1}html :where(.default\:col-span-10){grid-column:span 10 / span 10}html :where(.default\:col-span-11){grid-column:span 11 / span 11}html :where(.default\:col-span-12){grid-column:span 12 / span 12}html :where(.default\:col-span-2){grid-column:span 2 / span 2}html :where(.default\:col-span-3){grid-column:span 3 / span 3}html :where(.default\:col-span-4){grid-column:span 4 / span 4}html :where(.default\:col-span-5){grid-column:span 5 / span 5}html :where(.default\:col-span-6){grid-column:span 6 / span 6}html :where(.default\:col-span-7){grid-column:span 7 / span 7}html :where(.default\:col-span-8){grid-column:span 8 / span 8}html :where(.default\:col-span-9){grid-column:span 9 / span 9}html :where(.default\:col-span-full){grid-column:1 / -1}html :where(.default\:row-span-1){grid-row:span 1 / span 1}html :where(.default\:row-span-2){grid-row:span 2 / span 2}html :where(.default\:row-span-3){grid-row:span 3 / span 3}html :where(.default\:row-span-4){grid-row:span 4 / span 4}html :where(.default\:row-span-5){grid-row:span 5 / span 5}html :where(.default\:row-span-6){grid-row:span 6 / span 6}html :where(.default\:row-span-full){grid-row:1 / -1}html :where(.default\:grid-cols-1){grid-template-columns:repeat(1,minmax(0,1fr))}html :where(.default\:grid-cols-10){grid-template-columns:repeat(10,minmax(0,1fr))}html :where(.default\:grid-cols-11){grid-template-columns:repeat(11,minmax(0,1fr))}html :where(.default\:grid-cols-12){grid-template-columns:repeat(12,minmax(0,1fr))}html :where(.default\:grid-cols-2){grid-template-columns:repeat(2,minmax(0,1fr))}html :where(.default\:grid-cols-3){grid-template-columns:repeat(3,minmax(0,1fr))}html :where(.default\:grid-cols-4){grid-template-columns:repeat(4,minmax(0,1fr))}html :where(.default\:grid-cols-5){grid-template-columns:repeat(5,minmax(0,1fr))}html :where(.default\:grid-cols-6){grid-template-columns:repeat(6,minmax(0,1fr))}html :where(.default\:grid-cols-7){grid-template-columns:repeat(7,minmax(0,1fr))}html :where(.default\:grid-cols-8){grid-template-columns:repeat(8,minmax(0,1fr))}html :where(.default\:grid-cols-9){grid-template-columns:repeat(9,minmax(0,1fr))}html :where(.default\:gap-6){gap:1.5rem}html :where(.default\:text-left){text-align:left}:is(.dark .dark\:block){display:block}:is(.dark .dark\:hidden){display:none}:is(.dark .dark\:border-blue-700){--tw-border-opacity: 1;border-color:rgb(29 78 216 / var(--tw-border-opacity))}:is(.dark .dark\:border-gray-500){--tw-border-opacity: 1;border-color:rgb(107 114 128 / var(--tw-border-opacity))}:is(.dark .dark\:border-gray-700){--tw-border-opacity: 1;border-color:rgb(55 65 81 / var(--tw-border-opacity))}:is(.dark .dark\:border-gray-900){--tw-border-opacity: 1;border-color:rgb(17 24 39 / var(--tw-border-opacity))}:is(.dark .dark\:border-purple-700){--tw-border-opacity: 1;border-color:rgb(126 34 206 / var(--tw-border-opacity))}:is(.dark .dark\:border-red-700){--tw-border-opacity: 1;border-color:rgb(185 28 28 / var(--tw-border-opacity))}:is(.dark .dark\:bg-blue-900){--tw-bg-opacity: 1;background-color:rgb(30 58 138 / var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-700){--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-800){--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-800\/50){background-color:#1f293780}:is(.dark .dark\:bg-gray-900){--tw-bg-opacity: 1;background-color:rgb(17 24 39 / var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-950){--tw-bg-opacity: 1;background-color:rgb(3 7 18 / var(--tw-bg-opacity))}:is(.dark .dark\:bg-purple-900){--tw-bg-opacity: 1;background-color:rgb(88 28 135 / var(--tw-bg-opacity))}:is(.dark .dark\:bg-red-900){--tw-bg-opacity: 1;background-color:rgb(127 29 29 / var(--tw-bg-opacity))}:is(.dark .dark\:from-gray-900){--tw-gradient-from: #111827 var(--tw-gradient-from-position);--tw-gradient-to: rgb(17 24 39 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}:is(.dark .dark\:to-gray-800){--tw-gradient-to: #1f2937 var(--tw-gradient-to-position)}:is(.dark .dark\:stroke-gray-400){stroke:#9ca3af}:is(.dark .dark\:stroke-gray-600){stroke:#4b5563}:is(.dark .dark\:stroke-gray-700){stroke:#374151}:is(.dark .dark\:text-blue-300){--tw-text-opacity: 1;color:rgb(147 197 253 / var(--tw-text-opacity))}:is(.dark .dark\:text-gray-100){--tw-text-opacity: 1;color:rgb(243 244 246 / var(--tw-text-opacity))}:is(.dark .dark\:text-gray-200){--tw-text-opacity: 1;color:rgb(229 231 235 / var(--tw-text-opacity))}:is(.dark .dark\:text-gray-300){--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity))}:is(.dark .dark\:text-gray-400){--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity))}:is(.dark .dark\:text-gray-500){--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}:is(.dark .dark\:text-gray-600){--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity))}:is(.dark .dark\:text-purple-300){--tw-text-opacity: 1;color:rgb(216 180 254 / var(--tw-text-opacity))}:is(.dark .dark\:text-red-300){--tw-text-opacity: 1;color:rgb(252 165 165 / var(--tw-text-opacity))}:is(.dark .dark\:ring-gray-100\/10){--tw-ring-color: rgb(243 244 246 / .1)}:is(.dark .dark\:hover\:bg-gray-700:hover){--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-gray-800:hover){--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity))}:is(.dark .dark\:hover\:text-gray-500:hover){--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}:is(.dark .dark\:focus\:text-gray-500:focus){--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}@media (min-width: 640px){.sm\:col-span-1{grid-column:span 1 / span 1}.sm\:col-span-10{grid-column:span 10 / span 10}.sm\:col-span-11{grid-column:span 11 / span 11}.sm\:col-span-12{grid-column:span 12 / span 12}.sm\:col-span-2{grid-column:span 2 / span 2}.sm\:col-span-3{grid-column:span 3 / span 3}.sm\:col-span-4{grid-column:span 4 / span 4}.sm\:col-span-5{grid-column:span 5 / span 5}.sm\:col-span-6{grid-column:span 6 / span 6}.sm\:col-span-7{grid-column:span 7 / span 7}.sm\:col-span-8{grid-column:span 8 / span 8}.sm\:col-span-9{grid-column:span 9 / span 9}.sm\:col-span-full{grid-column:1 / -1}.sm\:row-span-1{grid-row:span 1 / span 1}.sm\:row-span-2{grid-row:span 2 / span 2}.sm\:row-span-3{grid-row:span 3 / span 3}.sm\:row-span-4{grid-row:span 4 / span 4}.sm\:row-span-5{grid-row:span 5 / span 5}.sm\:row-span-6{grid-row:span 6 / span 6}.sm\:row-span-full{grid-row:1 / -1}.sm\:h-0{height:0px}.sm\:h-0\.5{height:.125rem}.sm\:h-1{height:.25rem}.sm\:h-1\.5{height:.375rem}.sm\:h-1\/2{height:50%}.sm\:h-1\/3{height:33.333333%}.sm\:h-1\/4{height:25%}.sm\:h-1\/5{height:20%}.sm\:h-1\/6{height:16.666667%}.sm\:h-10{height:2.5rem}.sm\:h-11{height:2.75rem}.sm\:h-12{height:3rem}.sm\:h-128{height:32rem}.sm\:h-14{height:3.5rem}.sm\:h-16{height:4rem}.sm\:h-2{height:.5rem}.sm\:h-2\.5{height:.625rem}.sm\:h-2\/3{height:66.666667%}.sm\:h-2\/4{height:50%}.sm\:h-2\/5{height:40%}.sm\:h-2\/6{height:33.333333%}.sm\:h-20{height:5rem}.sm\:h-24{height:6rem}.sm\:h-28{height:7rem}.sm\:h-3{height:.75rem}.sm\:h-3\.5{height:.875rem}.sm\:h-3\/4{height:75%}.sm\:h-3\/5{height:60%}.sm\:h-3\/6{height:50%}.sm\:h-32{height:8rem}.sm\:h-36{height:9rem}.sm\:h-4{height:1rem}.sm\:h-4\/5{height:80%}.sm\:h-4\/6{height:66.666667%}.sm\:h-40{height:10rem}.sm\:h-44{height:11rem}.sm\:h-48{height:12rem}.sm\:h-5{height:1.25rem}.sm\:h-5\/6{height:83.333333%}.sm\:h-52{height:13rem}.sm\:h-56{height:14rem}.sm\:h-6{height:1.5rem}.sm\:h-60{height:15rem}.sm\:h-64{height:16rem}.sm\:h-7{height:1.75rem}.sm\:h-72{height:18rem}.sm\:h-8{height:2rem}.sm\:h-80{height:20rem}.sm\:h-9{height:2.25rem}.sm\:h-96{height:24rem}.sm\:max-h-0{max-height:0px}.sm\:max-h-0\.5{max-height:.125rem}.sm\:max-h-1{max-height:.25rem}.sm\:max-h-1\.5{max-height:.375rem}.sm\:max-h-10{max-height:2.5rem}.sm\:max-h-11{max-height:2.75rem}.sm\:max-h-12{max-height:3rem}.sm\:max-h-14{max-height:3.5rem}.sm\:max-h-16{max-height:4rem}.sm\:max-h-2{max-height:.5rem}.sm\:max-h-2\.5{max-height:.625rem}.sm\:max-h-20{max-height:5rem}.sm\:max-h-24{max-height:6rem}.sm\:max-h-28{max-height:7rem}.sm\:max-h-3{max-height:.75rem}.sm\:max-h-3\.5{max-height:.875rem}.sm\:max-h-32{max-height:8rem}.sm\:max-h-36{max-height:9rem}.sm\:max-h-4{max-height:1rem}.sm\:max-h-40{max-height:10rem}.sm\:max-h-44{max-height:11rem}.sm\:max-h-48{max-height:12rem}.sm\:max-h-5{max-height:1.25rem}.sm\:max-h-52{max-height:13rem}.sm\:max-h-56{max-height:14rem}.sm\:max-h-6{max-height:1.5rem}.sm\:max-h-60{max-height:15rem}.sm\:max-h-64{max-height:16rem}.sm\:max-h-7{max-height:1.75rem}.sm\:max-h-72{max-height:18rem}.sm\:max-h-8{max-height:2rem}.sm\:max-h-80{max-height:20rem}.sm\:max-h-9{max-height:2.25rem}.sm\:max-h-96{max-height:24rem}.sm\:min-h-0{min-height:0px}.sm\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.sm\:grid-cols-10{grid-template-columns:repeat(10,minmax(0,1fr))}.sm\:grid-cols-11{grid-template-columns:repeat(11,minmax(0,1fr))}.sm\:grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.sm\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.sm\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.sm\:grid-cols-7{grid-template-columns:repeat(7,minmax(0,1fr))}.sm\:grid-cols-8{grid-template-columns:repeat(8,minmax(0,1fr))}.sm\:grid-cols-9{grid-template-columns:repeat(9,minmax(0,1fr))}.sm\:gap-6{gap:1.5rem}.sm\:p-6{padding:1.5rem}.sm\:py-5{padding-top:1.25rem;padding-bottom:1.25rem}.sm\:text-2xl{font-size:1.5rem;line-height:2rem}.sm\:text-lg{font-size:1.125rem;line-height:1.75rem}.sm\:text-sm{font-size:.875rem;line-height:1.25rem}}@media (min-width: 768px){.md\:col-span-1{grid-column:span 1 / span 1}.md\:col-span-10{grid-column:span 10 / span 10}.md\:col-span-11{grid-column:span 11 / span 11}.md\:col-span-12{grid-column:span 12 / span 12}.md\:col-span-2{grid-column:span 2 / span 2}.md\:col-span-3{grid-column:span 3 / span 3}.md\:col-span-4{grid-column:span 4 / span 4}.md\:col-span-5{grid-column:span 5 / span 5}.md\:col-span-6{grid-column:span 6 / span 6}.md\:col-span-7{grid-column:span 7 / span 7}.md\:col-span-8{grid-column:span 8 / span 8}.md\:col-span-9{grid-column:span 9 / span 9}.md\:col-span-full{grid-column:1 / -1}.md\:row-span-1{grid-row:span 1 / span 1}.md\:row-span-2{grid-row:span 2 / span 2}.md\:row-span-3{grid-row:span 3 / span 3}.md\:row-span-4{grid-row:span 4 / span 4}.md\:row-span-5{grid-row:span 5 / span 5}.md\:row-span-6{grid-row:span 6 / span 6}.md\:row-span-full{grid-row:1 / -1}.md\:h-0{height:0px}.md\:h-0\.5{height:.125rem}.md\:h-1{height:.25rem}.md\:h-1\.5{height:.375rem}.md\:h-1\/2{height:50%}.md\:h-1\/3{height:33.333333%}.md\:h-1\/4{height:25%}.md\:h-1\/5{height:20%}.md\:h-1\/6{height:16.666667%}.md\:h-10{height:2.5rem}.md\:h-11{height:2.75rem}.md\:h-12{height:3rem}.md\:h-128{height:32rem}.md\:h-14{height:3.5rem}.md\:h-16{height:4rem}.md\:h-2{height:.5rem}.md\:h-2\.5{height:.625rem}.md\:h-2\/3{height:66.666667%}.md\:h-2\/4{height:50%}.md\:h-2\/5{height:40%}.md\:h-2\/6{height:33.333333%}.md\:h-20{height:5rem}.md\:h-24{height:6rem}.md\:h-28{height:7rem}.md\:h-3{height:.75rem}.md\:h-3\.5{height:.875rem}.md\:h-3\/4{height:75%}.md\:h-3\/5{height:60%}.md\:h-3\/6{height:50%}.md\:h-32{height:8rem}.md\:h-36{height:9rem}.md\:h-4{height:1rem}.md\:h-4\/5{height:80%}.md\:h-4\/6{height:66.666667%}.md\:h-40{height:10rem}.md\:h-44{height:11rem}.md\:h-48{height:12rem}.md\:h-5{height:1.25rem}.md\:h-5\/6{height:83.333333%}.md\:h-52{height:13rem}.md\:h-56{height:14rem}.md\:h-6{height:1.5rem}.md\:h-60{height:15rem}.md\:h-64{height:16rem}.md\:h-7{height:1.75rem}.md\:h-72{height:18rem}.md\:h-8{height:2rem}.md\:h-80{height:20rem}.md\:h-9{height:2.25rem}.md\:h-96{height:24rem}.md\:max-h-0{max-height:0px}.md\:max-h-0\.5{max-height:.125rem}.md\:max-h-1{max-height:.25rem}.md\:max-h-1\.5{max-height:.375rem}.md\:max-h-10{max-height:2.5rem}.md\:max-h-11{max-height:2.75rem}.md\:max-h-12{max-height:3rem}.md\:max-h-14{max-height:3.5rem}.md\:max-h-16{max-height:4rem}.md\:max-h-2{max-height:.5rem}.md\:max-h-2\.5{max-height:.625rem}.md\:max-h-20{max-height:5rem}.md\:max-h-24{max-height:6rem}.md\:max-h-28{max-height:7rem}.md\:max-h-3{max-height:.75rem}.md\:max-h-3\.5{max-height:.875rem}.md\:max-h-32{max-height:8rem}.md\:max-h-36{max-height:9rem}.md\:max-h-4{max-height:1rem}.md\:max-h-40{max-height:10rem}.md\:max-h-44{max-height:11rem}.md\:max-h-48{max-height:12rem}.md\:max-h-5{max-height:1.25rem}.md\:max-h-52{max-height:13rem}.md\:max-h-56{max-height:14rem}.md\:max-h-6{max-height:1.5rem}.md\:max-h-60{max-height:15rem}.md\:max-h-64{max-height:16rem}.md\:max-h-7{max-height:1.75rem}.md\:max-h-72{max-height:18rem}.md\:max-h-8{max-height:2rem}.md\:max-h-80{max-height:20rem}.md\:max-h-9{max-height:2.25rem}.md\:max-h-96{max-height:24rem}.md\:min-h-0{min-height:0px}.md\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.md\:grid-cols-10{grid-template-columns:repeat(10,minmax(0,1fr))}.md\:grid-cols-11{grid-template-columns:repeat(11,minmax(0,1fr))}.md\:grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.md\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.md\:grid-cols-7{grid-template-columns:repeat(7,minmax(0,1fr))}.md\:grid-cols-8{grid-template-columns:repeat(8,minmax(0,1fr))}.md\:grid-cols-9{grid-template-columns:repeat(9,minmax(0,1fr))}}@media (min-width: 1024px){.lg\:col-span-1{grid-column:span 1 / span 1}.lg\:col-span-10{grid-column:span 10 / span 10}.lg\:col-span-11{grid-column:span 11 / span 11}.lg\:col-span-12{grid-column:span 12 / span 12}.lg\:col-span-2{grid-column:span 2 / span 2}.lg\:col-span-3{grid-column:span 3 / span 3}.lg\:col-span-4{grid-column:span 4 / span 4}.lg\:col-span-5{grid-column:span 5 / span 5}.lg\:col-span-6{grid-column:span 6 / span 6}.lg\:col-span-7{grid-column:span 7 / span 7}.lg\:col-span-8{grid-column:span 8 / span 8}.lg\:col-span-9{grid-column:span 9 / span 9}.lg\:col-span-full{grid-column:1 / -1}.lg\:row-span-1{grid-row:span 1 / span 1}.lg\:row-span-2{grid-row:span 2 / span 2}.lg\:row-span-3{grid-row:span 3 / span 3}.lg\:row-span-4{grid-row:span 4 / span 4}.lg\:row-span-5{grid-row:span 5 / span 5}.lg\:row-span-6{grid-row:span 6 / span 6}.lg\:row-span-full{grid-row:1 / -1}.lg\:h-0{height:0px}.lg\:h-0\.5{height:.125rem}.lg\:h-1{height:.25rem}.lg\:h-1\.5{height:.375rem}.lg\:h-1\/2{height:50%}.lg\:h-1\/3{height:33.333333%}.lg\:h-1\/4{height:25%}.lg\:h-1\/5{height:20%}.lg\:h-1\/6{height:16.666667%}.lg\:h-10{height:2.5rem}.lg\:h-11{height:2.75rem}.lg\:h-12{height:3rem}.lg\:h-128{height:32rem}.lg\:h-14{height:3.5rem}.lg\:h-16{height:4rem}.lg\:h-2{height:.5rem}.lg\:h-2\.5{height:.625rem}.lg\:h-2\/3{height:66.666667%}.lg\:h-2\/4{height:50%}.lg\:h-2\/5{height:40%}.lg\:h-2\/6{height:33.333333%}.lg\:h-20{height:5rem}.lg\:h-24{height:6rem}.lg\:h-28{height:7rem}.lg\:h-3{height:.75rem}.lg\:h-3\.5{height:.875rem}.lg\:h-3\/4{height:75%}.lg\:h-3\/5{height:60%}.lg\:h-3\/6{height:50%}.lg\:h-32{height:8rem}.lg\:h-36{height:9rem}.lg\:h-4{height:1rem}.lg\:h-4\/5{height:80%}.lg\:h-4\/6{height:66.666667%}.lg\:h-40{height:10rem}.lg\:h-44{height:11rem}.lg\:h-48{height:12rem}.lg\:h-5{height:1.25rem}.lg\:h-5\/6{height:83.333333%}.lg\:h-52{height:13rem}.lg\:h-56{height:14rem}.lg\:h-6{height:1.5rem}.lg\:h-60{height:15rem}.lg\:h-64{height:16rem}.lg\:h-7{height:1.75rem}.lg\:h-72{height:18rem}.lg\:h-8{height:2rem}.lg\:h-80{height:20rem}.lg\:h-9{height:2.25rem}.lg\:h-96{height:24rem}.lg\:max-h-0{max-height:0px}.lg\:max-h-0\.5{max-height:.125rem}.lg\:max-h-1{max-height:.25rem}.lg\:max-h-1\.5{max-height:.375rem}.lg\:max-h-10{max-height:2.5rem}.lg\:max-h-11{max-height:2.75rem}.lg\:max-h-12{max-height:3rem}.lg\:max-h-14{max-height:3.5rem}.lg\:max-h-16{max-height:4rem}.lg\:max-h-2{max-height:.5rem}.lg\:max-h-2\.5{max-height:.625rem}.lg\:max-h-20{max-height:5rem}.lg\:max-h-24{max-height:6rem}.lg\:max-h-28{max-height:7rem}.lg\:max-h-3{max-height:.75rem}.lg\:max-h-3\.5{max-height:.875rem}.lg\:max-h-32{max-height:8rem}.lg\:max-h-36{max-height:9rem}.lg\:max-h-4{max-height:1rem}.lg\:max-h-40{max-height:10rem}.lg\:max-h-44{max-height:11rem}.lg\:max-h-48{max-height:12rem}.lg\:max-h-5{max-height:1.25rem}.lg\:max-h-52{max-height:13rem}.lg\:max-h-56{max-height:14rem}.lg\:max-h-6{max-height:1.5rem}.lg\:max-h-60{max-height:15rem}.lg\:max-h-64{max-height:16rem}.lg\:max-h-7{max-height:1.75rem}.lg\:max-h-72{max-height:18rem}.lg\:max-h-8{max-height:2rem}.lg\:max-h-80{max-height:20rem}.lg\:max-h-9{max-height:2.25rem}.lg\:max-h-96{max-height:24rem}.lg\:min-h-0{min-height:0px}.lg\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.lg\:grid-cols-10{grid-template-columns:repeat(10,minmax(0,1fr))}.lg\:grid-cols-11{grid-template-columns:repeat(11,minmax(0,1fr))}.lg\:grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.lg\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.lg\:grid-cols-7{grid-template-columns:repeat(7,minmax(0,1fr))}.lg\:grid-cols-8{grid-template-columns:repeat(8,minmax(0,1fr))}.lg\:grid-cols-9{grid-template-columns:repeat(9,minmax(0,1fr))}html :where(.default\:lg\:col-span-1){grid-column:span 1 / span 1}html :where(.default\:lg\:col-span-10){grid-column:span 10 / span 10}html :where(.default\:lg\:col-span-11){grid-column:span 11 / span 11}html :where(.default\:lg\:col-span-12){grid-column:span 12 / span 12}html :where(.default\:lg\:col-span-2){grid-column:span 2 / span 2}html :where(.default\:lg\:col-span-3){grid-column:span 3 / span 3}html :where(.default\:lg\:col-span-4){grid-column:span 4 / span 4}html :where(.default\:lg\:col-span-5){grid-column:span 5 / span 5}html :where(.default\:lg\:col-span-6){grid-column:span 6 / span 6}html :where(.default\:lg\:col-span-7){grid-column:span 7 / span 7}html :where(.default\:lg\:col-span-8){grid-column:span 8 / span 8}html :where(.default\:lg\:col-span-9){grid-column:span 9 / span 9}html :where(.default\:lg\:col-span-full){grid-column:1 / -1}html :where(.default\:lg\:row-span-1){grid-row:span 1 / span 1}html :where(.default\:lg\:row-span-2){grid-row:span 2 / span 2}html :where(.default\:lg\:row-span-3){grid-row:span 3 / span 3}html :where(.default\:lg\:row-span-4){grid-row:span 4 / span 4}html :where(.default\:lg\:row-span-5){grid-row:span 5 / span 5}html :where(.default\:lg\:row-span-6){grid-row:span 6 / span 6}html :where(.default\:lg\:row-span-full){grid-row:1 / -1}html :where(.default\:lg\:grid-cols-1){grid-template-columns:repeat(1,minmax(0,1fr))}html :where(.default\:lg\:grid-cols-10){grid-template-columns:repeat(10,minmax(0,1fr))}html :where(.default\:lg\:grid-cols-11){grid-template-columns:repeat(11,minmax(0,1fr))}html :where(.default\:lg\:grid-cols-12){grid-template-columns:repeat(12,minmax(0,1fr))}html :where(.default\:lg\:grid-cols-2){grid-template-columns:repeat(2,minmax(0,1fr))}html :where(.default\:lg\:grid-cols-3){grid-template-columns:repeat(3,minmax(0,1fr))}html :where(.default\:lg\:grid-cols-4){grid-template-columns:repeat(4,minmax(0,1fr))}html :where(.default\:lg\:grid-cols-5){grid-template-columns:repeat(5,minmax(0,1fr))}html :where(.default\:lg\:grid-cols-6){grid-template-columns:repeat(6,minmax(0,1fr))}html :where(.default\:lg\:grid-cols-7){grid-template-columns:repeat(7,minmax(0,1fr))}html :where(.default\:lg\:grid-cols-8){grid-template-columns:repeat(8,minmax(0,1fr))}html :where(.default\:lg\:grid-cols-9){grid-template-columns:repeat(9,minmax(0,1fr))}}@media (min-width: 1280px){.xl\:col-span-1{grid-column:span 1 / span 1}.xl\:col-span-10{grid-column:span 10 / span 10}.xl\:col-span-11{grid-column:span 11 / span 11}.xl\:col-span-12{grid-column:span 12 / span 12}.xl\:col-span-2{grid-column:span 2 / span 2}.xl\:col-span-3{grid-column:span 3 / span 3}.xl\:col-span-4{grid-column:span 4 / span 4}.xl\:col-span-5{grid-column:span 5 / span 5}.xl\:col-span-6{grid-column:span 6 / span 6}.xl\:col-span-7{grid-column:span 7 / span 7}.xl\:col-span-8{grid-column:span 8 / span 8}.xl\:col-span-9{grid-column:span 9 / span 9}.xl\:col-span-full{grid-column:1 / -1}.xl\:row-span-1{grid-row:span 1 / span 1}.xl\:row-span-2{grid-row:span 2 / span 2}.xl\:row-span-3{grid-row:span 3 / span 3}.xl\:row-span-4{grid-row:span 4 / span 4}.xl\:row-span-5{grid-row:span 5 / span 5}.xl\:row-span-6{grid-row:span 6 / span 6}.xl\:row-span-full{grid-row:1 / -1}.xl\:h-0{height:0px}.xl\:h-0\.5{height:.125rem}.xl\:h-1{height:.25rem}.xl\:h-1\.5{height:.375rem}.xl\:h-1\/2{height:50%}.xl\:h-1\/3{height:33.333333%}.xl\:h-1\/4{height:25%}.xl\:h-1\/5{height:20%}.xl\:h-1\/6{height:16.666667%}.xl\:h-10{height:2.5rem}.xl\:h-11{height:2.75rem}.xl\:h-12{height:3rem}.xl\:h-128{height:32rem}.xl\:h-14{height:3.5rem}.xl\:h-16{height:4rem}.xl\:h-2{height:.5rem}.xl\:h-2\.5{height:.625rem}.xl\:h-2\/3{height:66.666667%}.xl\:h-2\/4{height:50%}.xl\:h-2\/5{height:40%}.xl\:h-2\/6{height:33.333333%}.xl\:h-20{height:5rem}.xl\:h-24{height:6rem}.xl\:h-28{height:7rem}.xl\:h-3{height:.75rem}.xl\:h-3\.5{height:.875rem}.xl\:h-3\/4{height:75%}.xl\:h-3\/5{height:60%}.xl\:h-3\/6{height:50%}.xl\:h-32{height:8rem}.xl\:h-36{height:9rem}.xl\:h-4{height:1rem}.xl\:h-4\/5{height:80%}.xl\:h-4\/6{height:66.666667%}.xl\:h-40{height:10rem}.xl\:h-44{height:11rem}.xl\:h-48{height:12rem}.xl\:h-5{height:1.25rem}.xl\:h-5\/6{height:83.333333%}.xl\:h-52{height:13rem}.xl\:h-56{height:14rem}.xl\:h-6{height:1.5rem}.xl\:h-60{height:15rem}.xl\:h-64{height:16rem}.xl\:h-7{height:1.75rem}.xl\:h-72{height:18rem}.xl\:h-8{height:2rem}.xl\:h-80{height:20rem}.xl\:h-9{height:2.25rem}.xl\:h-96{height:24rem}.xl\:max-h-0{max-height:0px}.xl\:max-h-0\.5{max-height:.125rem}.xl\:max-h-1{max-height:.25rem}.xl\:max-h-1\.5{max-height:.375rem}.xl\:max-h-10{max-height:2.5rem}.xl\:max-h-11{max-height:2.75rem}.xl\:max-h-12{max-height:3rem}.xl\:max-h-14{max-height:3.5rem}.xl\:max-h-16{max-height:4rem}.xl\:max-h-2{max-height:.5rem}.xl\:max-h-2\.5{max-height:.625rem}.xl\:max-h-20{max-height:5rem}.xl\:max-h-24{max-height:6rem}.xl\:max-h-28{max-height:7rem}.xl\:max-h-3{max-height:.75rem}.xl\:max-h-3\.5{max-height:.875rem}.xl\:max-h-32{max-height:8rem}.xl\:max-h-36{max-height:9rem}.xl\:max-h-4{max-height:1rem}.xl\:max-h-40{max-height:10rem}.xl\:max-h-44{max-height:11rem}.xl\:max-h-48{max-height:12rem}.xl\:max-h-5{max-height:1.25rem}.xl\:max-h-52{max-height:13rem}.xl\:max-h-56{max-height:14rem}.xl\:max-h-6{max-height:1.5rem}.xl\:max-h-60{max-height:15rem}.xl\:max-h-64{max-height:16rem}.xl\:max-h-7{max-height:1.75rem}.xl\:max-h-72{max-height:18rem}.xl\:max-h-8{max-height:2rem}.xl\:max-h-80{max-height:20rem}.xl\:max-h-9{max-height:2.25rem}.xl\:max-h-96{max-height:24rem}.xl\:min-h-0{min-height:0px}.xl\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.xl\:grid-cols-10{grid-template-columns:repeat(10,minmax(0,1fr))}.xl\:grid-cols-11{grid-template-columns:repeat(11,minmax(0,1fr))}.xl\:grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.xl\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.xl\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.xl\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.xl\:grid-cols-7{grid-template-columns:repeat(7,minmax(0,1fr))}.xl\:grid-cols-8{grid-template-columns:repeat(8,minmax(0,1fr))}.xl\:grid-cols-9{grid-template-columns:repeat(9,minmax(0,1fr))}.xl\:pr-12{padding-right:3rem}}@media (min-width: 1536px){.\32xl\:col-span-1{grid-column:span 1 / span 1}.\32xl\:col-span-10{grid-column:span 10 / span 10}.\32xl\:col-span-11{grid-column:span 11 / span 11}.\32xl\:col-span-12{grid-column:span 12 / span 12}.\32xl\:col-span-2{grid-column:span 2 / span 2}.\32xl\:col-span-3{grid-column:span 3 / span 3}.\32xl\:col-span-4{grid-column:span 4 / span 4}.\32xl\:col-span-5{grid-column:span 5 / span 5}.\32xl\:col-span-6{grid-column:span 6 / span 6}.\32xl\:col-span-7{grid-column:span 7 / span 7}.\32xl\:col-span-8{grid-column:span 8 / span 8}.\32xl\:col-span-9{grid-column:span 9 / span 9}.\32xl\:col-span-full{grid-column:1 / -1}.\32xl\:row-span-1{grid-row:span 1 / span 1}.\32xl\:row-span-2{grid-row:span 2 / span 2}.\32xl\:row-span-3{grid-row:span 3 / span 3}.\32xl\:row-span-4{grid-row:span 4 / span 4}.\32xl\:row-span-5{grid-row:span 5 / span 5}.\32xl\:row-span-6{grid-row:span 6 / span 6}.\32xl\:row-span-full{grid-row:1 / -1}.\32xl\:h-0{height:0px}.\32xl\:h-0\.5{height:.125rem}.\32xl\:h-1{height:.25rem}.\32xl\:h-1\.5{height:.375rem}.\32xl\:h-1\/2{height:50%}.\32xl\:h-1\/3{height:33.333333%}.\32xl\:h-1\/4{height:25%}.\32xl\:h-1\/5{height:20%}.\32xl\:h-1\/6{height:16.666667%}.\32xl\:h-10{height:2.5rem}.\32xl\:h-11{height:2.75rem}.\32xl\:h-12{height:3rem}.\32xl\:h-128{height:32rem}.\32xl\:h-14{height:3.5rem}.\32xl\:h-16{height:4rem}.\32xl\:h-2{height:.5rem}.\32xl\:h-2\.5{height:.625rem}.\32xl\:h-2\/3{height:66.666667%}.\32xl\:h-2\/4{height:50%}.\32xl\:h-2\/5{height:40%}.\32xl\:h-2\/6{height:33.333333%}.\32xl\:h-20{height:5rem}.\32xl\:h-24{height:6rem}.\32xl\:h-28{height:7rem}.\32xl\:h-3{height:.75rem}.\32xl\:h-3\.5{height:.875rem}.\32xl\:h-3\/4{height:75%}.\32xl\:h-3\/5{height:60%}.\32xl\:h-3\/6{height:50%}.\32xl\:h-32{height:8rem}.\32xl\:h-36{height:9rem}.\32xl\:h-4{height:1rem}.\32xl\:h-4\/5{height:80%}.\32xl\:h-4\/6{height:66.666667%}.\32xl\:h-40{height:10rem}.\32xl\:h-44{height:11rem}.\32xl\:h-48{height:12rem}.\32xl\:h-5{height:1.25rem}.\32xl\:h-5\/6{height:83.333333%}.\32xl\:h-52{height:13rem}.\32xl\:h-56{height:14rem}.\32xl\:h-6{height:1.5rem}.\32xl\:h-60{height:15rem}.\32xl\:h-64{height:16rem}.\32xl\:h-7{height:1.75rem}.\32xl\:h-72{height:18rem}.\32xl\:h-8{height:2rem}.\32xl\:h-80{height:20rem}.\32xl\:h-9{height:2.25rem}.\32xl\:h-96{height:24rem}.\32xl\:max-h-0{max-height:0px}.\32xl\:max-h-0\.5{max-height:.125rem}.\32xl\:max-h-1{max-height:.25rem}.\32xl\:max-h-1\.5{max-height:.375rem}.\32xl\:max-h-10{max-height:2.5rem}.\32xl\:max-h-11{max-height:2.75rem}.\32xl\:max-h-12{max-height:3rem}.\32xl\:max-h-14{max-height:3.5rem}.\32xl\:max-h-16{max-height:4rem}.\32xl\:max-h-2{max-height:.5rem}.\32xl\:max-h-2\.5{max-height:.625rem}.\32xl\:max-h-20{max-height:5rem}.\32xl\:max-h-24{max-height:6rem}.\32xl\:max-h-28{max-height:7rem}.\32xl\:max-h-3{max-height:.75rem}.\32xl\:max-h-3\.5{max-height:.875rem}.\32xl\:max-h-32{max-height:8rem}.\32xl\:max-h-36{max-height:9rem}.\32xl\:max-h-4{max-height:1rem}.\32xl\:max-h-40{max-height:10rem}.\32xl\:max-h-44{max-height:11rem}.\32xl\:max-h-48{max-height:12rem}.\32xl\:max-h-5{max-height:1.25rem}.\32xl\:max-h-52{max-height:13rem}.\32xl\:max-h-56{max-height:14rem}.\32xl\:max-h-6{max-height:1.5rem}.\32xl\:max-h-60{max-height:15rem}.\32xl\:max-h-64{max-height:16rem}.\32xl\:max-h-7{max-height:1.75rem}.\32xl\:max-h-72{max-height:18rem}.\32xl\:max-h-8{max-height:2rem}.\32xl\:max-h-80{max-height:20rem}.\32xl\:max-h-9{max-height:2.25rem}.\32xl\:max-h-96{max-height:24rem}.\32xl\:min-h-0{min-height:0px}.\32xl\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.\32xl\:grid-cols-10{grid-template-columns:repeat(10,minmax(0,1fr))}.\32xl\:grid-cols-11{grid-template-columns:repeat(11,minmax(0,1fr))}.\32xl\:grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.\32xl\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.\32xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.\32xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.\32xl\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.\32xl\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.\32xl\:grid-cols-7{grid-template-columns:repeat(7,minmax(0,1fr))}.\32xl\:grid-cols-8{grid-template-columns:repeat(8,minmax(0,1fr))}.\32xl\:grid-cols-9{grid-template-columns:repeat(9,minmax(0,1fr))}}.\[\&\>svg\]\:h-6>svg{height:1.5rem}.\[\&\>svg\]\:w-6>svg{width:1.5rem}.\[\&\>svg\]\:flex-shrink-0>svg{flex-shrink:0}.\[\&\>svg\]\:stroke-gray-400>svg{stroke:#9ca3af}:is(.dark .\[\&\>svg\]\:dark\:stroke-gray-600)>svg{stroke:#4b5563} +*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Figtree,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}[type=text],input:where(:not([type])),[type=email],[type=url],[type=password],[type=number],[type=date],[type=datetime-local],[type=month],[type=search],[type=tel],[type=time],[type=week],[multiple],textarea,select{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#6b7280;border-width:1px;border-radius:0;padding:.5rem .75rem;font-size:1rem;line-height:1.5rem;--tw-shadow: 0 0 #0000}[type=text]:focus,input:where(:not([type])):focus,[type=email]:focus,[type=url]:focus,[type=password]:focus,[type=number]:focus,[type=date]:focus,[type=datetime-local]:focus,[type=month]:focus,[type=search]:focus,[type=tel]:focus,[type=time]:focus,[type=week]:focus,[multiple]:focus,textarea:focus,select:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-inset: var(--tw-empty, );--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: #2563eb;--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);border-color:#2563eb}input::-moz-placeholder,textarea::-moz-placeholder{color:#6b7280;opacity:1}input::placeholder,textarea::placeholder{color:#6b7280;opacity:1}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-date-and-time-value{min-height:1.5em;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit,::-webkit-datetime-edit-year-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-meridiem-field{padding-top:0;padding-bottom:0}select{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;padding-right:2.5rem;-webkit-print-color-adjust:exact;print-color-adjust:exact}[multiple],[size]:where(select:not([size="1"])){background-image:initial;background-position:initial;background-repeat:unset;background-size:initial;padding-right:.75rem;-webkit-print-color-adjust:unset;print-color-adjust:unset}[type=checkbox],[type=radio]{-webkit-appearance:none;-moz-appearance:none;appearance:none;padding:0;-webkit-print-color-adjust:exact;print-color-adjust:exact;display:inline-block;vertical-align:middle;background-origin:border-box;-webkit-user-select:none;-moz-user-select:none;user-select:none;flex-shrink:0;height:1rem;width:1rem;color:#2563eb;background-color:#fff;border-color:#6b7280;border-width:1px;--tw-shadow: 0 0 #0000}[type=checkbox]{border-radius:0}[type=radio]{border-radius:100%}[type=checkbox]:focus,[type=radio]:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-inset: var(--tw-empty, );--tw-ring-offset-width: 2px;--tw-ring-offset-color: #fff;--tw-ring-color: #2563eb;--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}[type=checkbox]:checked,[type=radio]:checked{border-color:transparent;background-color:currentColor;background-size:100% 100%;background-position:center;background-repeat:no-repeat}[type=checkbox]:checked{background-image:url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e")}[type=radio]:checked{background-image:url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e")}[type=checkbox]:checked:hover,[type=checkbox]:checked:focus,[type=radio]:checked:hover,[type=radio]:checked:focus{border-color:transparent;background-color:currentColor}[type=checkbox]:indeterminate{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3e%3c/svg%3e");border-color:transparent;background-color:currentColor;background-size:100% 100%;background-position:center;background-repeat:no-repeat}[type=checkbox]:indeterminate:hover,[type=checkbox]:indeterminate:focus{border-color:transparent;background-color:currentColor}[type=file]{background:unset;border-color:inherit;border-width:0;border-radius:0;padding:0;font-size:unset;line-height:inherit}[type=file]:focus{outline:1px solid ButtonText;outline:1px auto -webkit-focus-ring-color}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.container{width:100%}@media (min-width: 640px){.container{max-width:640px}}@media (min-width: 768px){.container{max-width:768px}}@media (min-width: 1024px){.container{max-width:1024px}}@media (min-width: 1280px){.container{max-width:1280px}}@media (min-width: 1536px){.container{max-width:1536px}}.pointer-events-none{pointer-events:none}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.-left-px{left:-1px}.-top-2{top:-.5rem}.bottom-0{bottom:0}.left-0{left:0}.right-0{right:0}.top-0{top:0}.z-10{z-index:10}.col-span-1{grid-column:span 1 / span 1}.col-span-10{grid-column:span 10 / span 10}.col-span-11{grid-column:span 11 / span 11}.col-span-12{grid-column:span 12 / span 12}.col-span-2{grid-column:span 2 / span 2}.col-span-3{grid-column:span 3 / span 3}.col-span-4{grid-column:span 4 / span 4}.col-span-5{grid-column:span 5 / span 5}.col-span-6{grid-column:span 6 / span 6}.col-span-7{grid-column:span 7 / span 7}.col-span-8{grid-column:span 8 / span 8}.col-span-9{grid-column:span 9 / span 9}.col-span-full{grid-column:1 / -1}.row-span-1{grid-row:span 1 / span 1}.row-span-2{grid-row:span 2 / span 2}.row-span-3{grid-row:span 3 / span 3}.row-span-4{grid-row:span 4 / span 4}.row-span-5{grid-row:span 5 / span 5}.row-span-6{grid-row:span 6 / span 6}.row-span-full{grid-row:1 / -1}.mx-auto{margin-left:auto;margin-right:auto}.mx-px{margin-left:1px;margin-right:1px}.mb-3{margin-bottom:.75rem}.mb-px{margin-bottom:1px}.ml-2{margin-left:.5rem}.mr-1{margin-right:.25rem}.mr-2{margin-right:.5rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.block{display:block}.flex{display:flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-0{height:0px}.h-0\.5{height:.125rem}.h-1{height:.25rem}.h-1\.5{height:.375rem}.h-1\/2{height:50%}.h-1\/3{height:33.333333%}.h-1\/4{height:25%}.h-1\/5{height:20%}.h-1\/6{height:16.666667%}.h-10{height:2.5rem}.h-11{height:2.75rem}.h-12{height:3rem}.h-128{height:32rem}.h-14{height:3.5rem}.h-16{height:4rem}.h-2{height:.5rem}.h-2\.5{height:.625rem}.h-2\/3{height:66.666667%}.h-2\/4{height:50%}.h-2\/5{height:40%}.h-2\/6{height:33.333333%}.h-20{height:5rem}.h-24{height:6rem}.h-28{height:7rem}.h-3{height:.75rem}.h-3\.5{height:.875rem}.h-3\/4{height:75%}.h-3\/5{height:60%}.h-3\/6{height:50%}.h-32{height:8rem}.h-36{height:9rem}.h-4{height:1rem}.h-4\/5{height:80%}.h-4\/6{height:66.666667%}.h-40{height:10rem}.h-44{height:11rem}.h-48{height:12rem}.h-5{height:1.25rem}.h-5\/6{height:83.333333%}.h-52{height:13rem}.h-56{height:14rem}.h-6{height:1.5rem}.h-60{height:15rem}.h-64{height:16rem}.h-7{height:1.75rem}.h-72{height:18rem}.h-8{height:2rem}.h-80{height:20rem}.h-9{height:2.25rem}.h-96{height:24rem}.h-\[30px\]{height:30px}.h-\[52px\]{height:52px}.h-full{height:100%}.max-h-0{max-height:0px}.max-h-0\.5{max-height:.125rem}.max-h-1{max-height:.25rem}.max-h-1\.5{max-height:.375rem}.max-h-10{max-height:2.5rem}.max-h-11{max-height:2.75rem}.max-h-12{max-height:3rem}.max-h-14{max-height:3.5rem}.max-h-16{max-height:4rem}.max-h-2{max-height:.5rem}.max-h-2\.5{max-height:.625rem}.max-h-20{max-height:5rem}.max-h-24{max-height:6rem}.max-h-28{max-height:7rem}.max-h-3{max-height:.75rem}.max-h-3\.5{max-height:.875rem}.max-h-32{max-height:8rem}.max-h-36{max-height:9rem}.max-h-4{max-height:1rem}.max-h-40{max-height:10rem}.max-h-44{max-height:11rem}.max-h-48{max-height:12rem}.max-h-5{max-height:1.25rem}.max-h-52{max-height:13rem}.max-h-56{max-height:14rem}.max-h-6{max-height:1.5rem}.max-h-60{max-height:15rem}.max-h-64{max-height:16rem}.max-h-7{max-height:1.75rem}.max-h-72{max-height:18rem}.max-h-8{max-height:2rem}.max-h-80{max-height:20rem}.max-h-9{max-height:2.25rem}.max-h-96{max-height:24rem}.min-h-0{min-height:0px}.min-h-full{min-height:100%}.min-h-screen{min-height:100vh}.w-1{width:.25rem}.w-1\/12{width:8.333333%}.w-1\/2{width:50%}.w-14{width:3.5rem}.w-2\/12{width:16.666667%}.w-3{width:.75rem}.w-3\/12{width:25%}.w-36{width:9rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-8{width:2rem}.w-full{width:100%}.min-w-\[5rem\]{min-width:5rem}.max-w-\[1px\]{max-width:1px}.max-w-fit{max-width:-moz-fit-content;max-width:fit-content}.max-w-full{max-width:100%}.max-w-xs{max-width:20rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.flex-grow{flex-grow:1}.flex-grow-\[10000\]{flex-grow:10000}.basis-0{flex-basis:0px}.basis-56{flex-basis:14rem}.basis-full{flex-basis:100%}.origin-bottom{transform-origin:bottom}.origin-top-right{transform-origin:top right}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-10{grid-template-columns:repeat(10,minmax(0,1fr))}.grid-cols-11{grid-template-columns:repeat(11,minmax(0,1fr))}.grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.grid-cols-7{grid-template-columns:repeat(7,minmax(0,1fr))}.grid-cols-8{grid-template-columns:repeat(8,minmax(0,1fr))}.grid-cols-9{grid-template-columns:repeat(9,minmax(0,1fr))}.grid-cols-\[max-content\,minmax\(max-content\,1fr\)\,max-content\,minmax\(min-content\,2fr\)\,max-content\,minmax\(min-content\,2fr\)\,minmax\(max-content\,1fr\)\]{grid-template-columns:max-content minmax(max-content,1fr) max-content minmax(min-content,2fr) max-content minmax(min-content,2fr) minmax(max-content,1fr)}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.items-baseline{align-items:baseline}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.gap-8{gap:2rem}.gap-x-2{-moz-column-gap:.5rem;column-gap:.5rem}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.overflow-ellipsis{text-overflow:ellipsis}.whitespace-nowrap{white-space:nowrap}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.rounded-r-md{border-top-right-radius:.375rem;border-bottom-right-radius:.375rem}.border{border-width:1px}.border-0{border-width:0px}.border-b{border-bottom-width:1px}.border-r{border-right-width:1px}.border-blue-200{--tw-border-opacity: 1;border-color:rgb(191 219 254 / var(--tw-border-opacity))}.border-gray-200{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity))}.border-purple-200{--tw-border-opacity: 1;border-color:rgb(233 213 255 / var(--tw-border-opacity))}.border-red-200{--tw-border-opacity: 1;border-color:rgb(254 202 202 / var(--tw-border-opacity))}.bg-\[\#9333ea\]{--tw-bg-opacity: 1;background-color:rgb(147 51 234 / var(--tw-bg-opacity))}.bg-\[\#e11d48\]{--tw-bg-opacity: 1;background-color:rgb(225 29 72 / var(--tw-bg-opacity))}.bg-\[\#eab308\]{--tw-bg-opacity: 1;background-color:rgb(234 179 8 / var(--tw-bg-opacity))}.bg-\[rgba\(107\,114\,128\,0\.5\)\]{background-color:#6b728080}.bg-\[rgba\(147\,51\,234\,0\.5\)\]{background-color:#9333ea80}.bg-blue-50{--tw-bg-opacity: 1;background-color:rgb(239 246 255 / var(--tw-bg-opacity))}.bg-gray-100{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity))}.bg-gray-700{--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity))}.bg-green-500{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity))}.bg-purple-50{--tw-bg-opacity: 1;background-color:rgb(250 245 255 / var(--tw-bg-opacity))}.bg-purple-500{--tw-bg-opacity: 1;background-color:rgb(168 85 247 / var(--tw-bg-opacity))}.bg-red-50{--tw-bg-opacity: 1;background-color:rgb(254 242 242 / var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity))}.bg-gradient-to-r{background-image:linear-gradient(to right,var(--tw-gradient-stops))}.bg-gradient-to-t{background-image:linear-gradient(to top,var(--tw-gradient-stops))}.from-transparent{--tw-gradient-from: transparent var(--tw-gradient-from-position);--tw-gradient-to: rgb(0 0 0 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.from-white{--tw-gradient-from: #fff var(--tw-gradient-from-position);--tw-gradient-to: rgb(255 255 255 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.to-gray-700{--tw-gradient-to: #374151 var(--tw-gradient-to-position)}.stroke-gray-300{stroke:#d1d5db}.stroke-gray-400{stroke:#9ca3af}.stroke-gray-500{stroke:#6b7280}.stroke-red-500{stroke:#ef4444}.\!p-0{padding:0!important}.p-1{padding:.25rem}.p-3{padding:.75rem}.p-4{padding:1rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.pb-12{padding-bottom:3rem}.pb-px{padding-bottom:1px}.pl-3{padding-left:.75rem}.pr-8{padding-right:2rem}.pt-6{padding-top:1.5rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.font-sans{font-family:Figtree,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji"}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.tabular-nums{--tw-numeric-spacing: tabular-nums;font-variant-numeric:var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction)}.leading-none{line-height:1}.text-blue-400{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity))}.text-cyan-200{--tw-text-opacity: 1;color:rgb(165 243 252 / var(--tw-text-opacity))}.text-gray-100{--tw-text-opacity: 1;color:rgb(243 244 246 / var(--tw-text-opacity))}.text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity))}.text-orange-200{--tw-text-opacity: 1;color:rgb(254 215 170 / var(--tw-text-opacity))}.text-purple-200{--tw-text-opacity: 1;color:rgb(233 213 255 / var(--tw-text-opacity))}.text-purple-400{--tw-text-opacity: 1;color:rgb(192 132 252 / var(--tw-text-opacity))}.text-red-200{--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.opacity-25{opacity:.25}.shadow-none{--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.ring-1{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-gray-900\/5{--tw-ring-color: rgb(17 24 39 / .05)}.\@container{container-type:inline-size}.\@container\/scroll-wrapper{container-type:inline-size;container-name:scroll-wrapper}.\[scrollbar-color\:theme\(colors\.gray\.500\)_transparent\]{scrollbar-color:#6b7280 transparent}.\[scrollbar-width\:thin\]{scrollbar-width:thin}[x-cloak]{display:none}.after\:absolute:after{content:var(--tw-content);position:absolute}.after\:right-\[calc\(-1\*var\(--triangle-size\)\)\]:after{content:var(--tw-content);right:calc(-1 * var(--triangle-size))}.after\:top-\[calc\(50\%-var\(--triangle-size\)\)\]:after{content:var(--tw-content);top:calc(50% - var(--triangle-size))}.after\:border-b-\[length\:var\(--triangle-size\)\]:after{content:var(--tw-content);border-bottom-width:var(--triangle-size)}.after\:border-l-\[length\:var\(--triangle-size\)\]:after{content:var(--tw-content);border-left-width:var(--triangle-size)}.after\:border-t-\[length\:var\(--triangle-size\)\]:after{content:var(--tw-content);border-top-width:var(--triangle-size)}.after\:border-transparent:after{content:var(--tw-content);border-color:transparent}.after\:border-l-purple-500:after{content:var(--tw-content);--tw-border-opacity: 1;border-left-color:rgb(168 85 247 / var(--tw-border-opacity))}.after\:\[--triangle-size\:4px\]:after{content:var(--tw-content);--triangle-size: 4px}.first\:h-0:first-child{height:0px}.first\:rounded-l-md:first-child{border-top-left-radius:.375rem;border-bottom-left-radius:.375rem}.first\:pl-3:first-child{padding-left:.75rem}.last\:rounded-r-md:last-child{border-top-right-radius:.375rem;border-bottom-right-radius:.375rem}.last\:pr-3:last-child{padding-right:.75rem}.focus-within\:ring:focus-within{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.hover\:bg-gray-100:hover{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity))}.hover\:text-gray-400:hover{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity))}.hover\:text-gray-500:hover{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}.focus\:text-gray-500:focus{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}.focus\:ring-0:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}@container (min-width: 24rem){.\@sm\:block{display:block}.\@sm\:px-3{padding-left:.75rem;padding-right:.75rem}}@container (min-width: 28rem){.\@md\:mb-6{margin-bottom:1.5rem}}@container (min-width: 32rem){.\@lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@container (min-width: 48rem){.\@3xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}@container (min-width: 72rem){.\@6xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}html :where(.default\:col-span-1){grid-column:span 1 / span 1}html :where(.default\:col-span-10){grid-column:span 10 / span 10}html :where(.default\:col-span-11){grid-column:span 11 / span 11}html :where(.default\:col-span-12){grid-column:span 12 / span 12}html :where(.default\:col-span-2){grid-column:span 2 / span 2}html :where(.default\:col-span-3){grid-column:span 3 / span 3}html :where(.default\:col-span-4){grid-column:span 4 / span 4}html :where(.default\:col-span-5){grid-column:span 5 / span 5}html :where(.default\:col-span-6){grid-column:span 6 / span 6}html :where(.default\:col-span-7){grid-column:span 7 / span 7}html :where(.default\:col-span-8){grid-column:span 8 / span 8}html :where(.default\:col-span-9){grid-column:span 9 / span 9}html :where(.default\:col-span-full){grid-column:1 / -1}html :where(.default\:row-span-1){grid-row:span 1 / span 1}html :where(.default\:row-span-2){grid-row:span 2 / span 2}html :where(.default\:row-span-3){grid-row:span 3 / span 3}html :where(.default\:row-span-4){grid-row:span 4 / span 4}html :where(.default\:row-span-5){grid-row:span 5 / span 5}html :where(.default\:row-span-6){grid-row:span 6 / span 6}html :where(.default\:row-span-full){grid-row:1 / -1}html :where(.default\:grid-cols-1){grid-template-columns:repeat(1,minmax(0,1fr))}html :where(.default\:grid-cols-10){grid-template-columns:repeat(10,minmax(0,1fr))}html :where(.default\:grid-cols-11){grid-template-columns:repeat(11,minmax(0,1fr))}html :where(.default\:grid-cols-12){grid-template-columns:repeat(12,minmax(0,1fr))}html :where(.default\:grid-cols-2){grid-template-columns:repeat(2,minmax(0,1fr))}html :where(.default\:grid-cols-3){grid-template-columns:repeat(3,minmax(0,1fr))}html :where(.default\:grid-cols-4){grid-template-columns:repeat(4,minmax(0,1fr))}html :where(.default\:grid-cols-5){grid-template-columns:repeat(5,minmax(0,1fr))}html :where(.default\:grid-cols-6){grid-template-columns:repeat(6,minmax(0,1fr))}html :where(.default\:grid-cols-7){grid-template-columns:repeat(7,minmax(0,1fr))}html :where(.default\:grid-cols-8){grid-template-columns:repeat(8,minmax(0,1fr))}html :where(.default\:grid-cols-9){grid-template-columns:repeat(9,minmax(0,1fr))}html :where(.default\:gap-6){gap:1.5rem}html :where(.default\:text-left){text-align:left}:is(.dark .dark\:block){display:block}:is(.dark .dark\:hidden){display:none}:is(.dark .dark\:border-blue-700){--tw-border-opacity: 1;border-color:rgb(29 78 216 / var(--tw-border-opacity))}:is(.dark .dark\:border-gray-500){--tw-border-opacity: 1;border-color:rgb(107 114 128 / var(--tw-border-opacity))}:is(.dark .dark\:border-gray-700){--tw-border-opacity: 1;border-color:rgb(55 65 81 / var(--tw-border-opacity))}:is(.dark .dark\:border-gray-900){--tw-border-opacity: 1;border-color:rgb(17 24 39 / var(--tw-border-opacity))}:is(.dark .dark\:border-purple-700){--tw-border-opacity: 1;border-color:rgb(126 34 206 / var(--tw-border-opacity))}:is(.dark .dark\:border-red-700){--tw-border-opacity: 1;border-color:rgb(185 28 28 / var(--tw-border-opacity))}:is(.dark .dark\:bg-blue-900){--tw-bg-opacity: 1;background-color:rgb(30 58 138 / var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-700){--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-800){--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-800\/50){background-color:#1f293780}:is(.dark .dark\:bg-gray-900){--tw-bg-opacity: 1;background-color:rgb(17 24 39 / var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-950){--tw-bg-opacity: 1;background-color:rgb(3 7 18 / var(--tw-bg-opacity))}:is(.dark .dark\:bg-purple-900){--tw-bg-opacity: 1;background-color:rgb(88 28 135 / var(--tw-bg-opacity))}:is(.dark .dark\:bg-red-900){--tw-bg-opacity: 1;background-color:rgb(127 29 29 / var(--tw-bg-opacity))}:is(.dark .dark\:from-gray-900){--tw-gradient-from: #111827 var(--tw-gradient-from-position);--tw-gradient-to: rgb(17 24 39 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}:is(.dark .dark\:to-gray-800){--tw-gradient-to: #1f2937 var(--tw-gradient-to-position)}:is(.dark .dark\:stroke-gray-400){stroke:#9ca3af}:is(.dark .dark\:stroke-gray-600){stroke:#4b5563}:is(.dark .dark\:stroke-gray-700){stroke:#374151}:is(.dark .dark\:text-blue-300){--tw-text-opacity: 1;color:rgb(147 197 253 / var(--tw-text-opacity))}:is(.dark .dark\:text-gray-100){--tw-text-opacity: 1;color:rgb(243 244 246 / var(--tw-text-opacity))}:is(.dark .dark\:text-gray-200){--tw-text-opacity: 1;color:rgb(229 231 235 / var(--tw-text-opacity))}:is(.dark .dark\:text-gray-300){--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity))}:is(.dark .dark\:text-gray-400){--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity))}:is(.dark .dark\:text-gray-500){--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}:is(.dark .dark\:text-gray-600){--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity))}:is(.dark .dark\:text-purple-300){--tw-text-opacity: 1;color:rgb(216 180 254 / var(--tw-text-opacity))}:is(.dark .dark\:text-red-300){--tw-text-opacity: 1;color:rgb(252 165 165 / var(--tw-text-opacity))}:is(.dark .dark\:ring-gray-100\/10){--tw-ring-color: rgb(243 244 246 / .1)}:is(.dark .dark\:hover\:bg-gray-700:hover){--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-gray-800:hover){--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity))}:is(.dark .dark\:hover\:text-gray-500:hover){--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}:is(.dark .dark\:focus\:text-gray-500:focus){--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}@media (min-width: 640px){.sm\:col-span-1{grid-column:span 1 / span 1}.sm\:col-span-10{grid-column:span 10 / span 10}.sm\:col-span-11{grid-column:span 11 / span 11}.sm\:col-span-12{grid-column:span 12 / span 12}.sm\:col-span-2{grid-column:span 2 / span 2}.sm\:col-span-3{grid-column:span 3 / span 3}.sm\:col-span-4{grid-column:span 4 / span 4}.sm\:col-span-5{grid-column:span 5 / span 5}.sm\:col-span-6{grid-column:span 6 / span 6}.sm\:col-span-7{grid-column:span 7 / span 7}.sm\:col-span-8{grid-column:span 8 / span 8}.sm\:col-span-9{grid-column:span 9 / span 9}.sm\:col-span-full{grid-column:1 / -1}.sm\:row-span-1{grid-row:span 1 / span 1}.sm\:row-span-2{grid-row:span 2 / span 2}.sm\:row-span-3{grid-row:span 3 / span 3}.sm\:row-span-4{grid-row:span 4 / span 4}.sm\:row-span-5{grid-row:span 5 / span 5}.sm\:row-span-6{grid-row:span 6 / span 6}.sm\:row-span-full{grid-row:1 / -1}.sm\:h-0{height:0px}.sm\:h-0\.5{height:.125rem}.sm\:h-1{height:.25rem}.sm\:h-1\.5{height:.375rem}.sm\:h-1\/2{height:50%}.sm\:h-1\/3{height:33.333333%}.sm\:h-1\/4{height:25%}.sm\:h-1\/5{height:20%}.sm\:h-1\/6{height:16.666667%}.sm\:h-10{height:2.5rem}.sm\:h-11{height:2.75rem}.sm\:h-12{height:3rem}.sm\:h-128{height:32rem}.sm\:h-14{height:3.5rem}.sm\:h-16{height:4rem}.sm\:h-2{height:.5rem}.sm\:h-2\.5{height:.625rem}.sm\:h-2\/3{height:66.666667%}.sm\:h-2\/4{height:50%}.sm\:h-2\/5{height:40%}.sm\:h-2\/6{height:33.333333%}.sm\:h-20{height:5rem}.sm\:h-24{height:6rem}.sm\:h-28{height:7rem}.sm\:h-3{height:.75rem}.sm\:h-3\.5{height:.875rem}.sm\:h-3\/4{height:75%}.sm\:h-3\/5{height:60%}.sm\:h-3\/6{height:50%}.sm\:h-32{height:8rem}.sm\:h-36{height:9rem}.sm\:h-4{height:1rem}.sm\:h-4\/5{height:80%}.sm\:h-4\/6{height:66.666667%}.sm\:h-40{height:10rem}.sm\:h-44{height:11rem}.sm\:h-48{height:12rem}.sm\:h-5{height:1.25rem}.sm\:h-5\/6{height:83.333333%}.sm\:h-52{height:13rem}.sm\:h-56{height:14rem}.sm\:h-6{height:1.5rem}.sm\:h-60{height:15rem}.sm\:h-64{height:16rem}.sm\:h-7{height:1.75rem}.sm\:h-72{height:18rem}.sm\:h-8{height:2rem}.sm\:h-80{height:20rem}.sm\:h-9{height:2.25rem}.sm\:h-96{height:24rem}.sm\:max-h-0{max-height:0px}.sm\:max-h-0\.5{max-height:.125rem}.sm\:max-h-1{max-height:.25rem}.sm\:max-h-1\.5{max-height:.375rem}.sm\:max-h-10{max-height:2.5rem}.sm\:max-h-11{max-height:2.75rem}.sm\:max-h-12{max-height:3rem}.sm\:max-h-14{max-height:3.5rem}.sm\:max-h-16{max-height:4rem}.sm\:max-h-2{max-height:.5rem}.sm\:max-h-2\.5{max-height:.625rem}.sm\:max-h-20{max-height:5rem}.sm\:max-h-24{max-height:6rem}.sm\:max-h-28{max-height:7rem}.sm\:max-h-3{max-height:.75rem}.sm\:max-h-3\.5{max-height:.875rem}.sm\:max-h-32{max-height:8rem}.sm\:max-h-36{max-height:9rem}.sm\:max-h-4{max-height:1rem}.sm\:max-h-40{max-height:10rem}.sm\:max-h-44{max-height:11rem}.sm\:max-h-48{max-height:12rem}.sm\:max-h-5{max-height:1.25rem}.sm\:max-h-52{max-height:13rem}.sm\:max-h-56{max-height:14rem}.sm\:max-h-6{max-height:1.5rem}.sm\:max-h-60{max-height:15rem}.sm\:max-h-64{max-height:16rem}.sm\:max-h-7{max-height:1.75rem}.sm\:max-h-72{max-height:18rem}.sm\:max-h-8{max-height:2rem}.sm\:max-h-80{max-height:20rem}.sm\:max-h-9{max-height:2.25rem}.sm\:max-h-96{max-height:24rem}.sm\:min-h-0{min-height:0px}.sm\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.sm\:grid-cols-10{grid-template-columns:repeat(10,minmax(0,1fr))}.sm\:grid-cols-11{grid-template-columns:repeat(11,minmax(0,1fr))}.sm\:grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.sm\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.sm\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.sm\:grid-cols-7{grid-template-columns:repeat(7,minmax(0,1fr))}.sm\:grid-cols-8{grid-template-columns:repeat(8,minmax(0,1fr))}.sm\:grid-cols-9{grid-template-columns:repeat(9,minmax(0,1fr))}.sm\:gap-6{gap:1.5rem}.sm\:p-6{padding:1.5rem}.sm\:py-5{padding-top:1.25rem;padding-bottom:1.25rem}.sm\:text-2xl{font-size:1.5rem;line-height:2rem}.sm\:text-lg{font-size:1.125rem;line-height:1.75rem}.sm\:text-sm{font-size:.875rem;line-height:1.25rem}}@media (min-width: 768px){.md\:col-span-1{grid-column:span 1 / span 1}.md\:col-span-10{grid-column:span 10 / span 10}.md\:col-span-11{grid-column:span 11 / span 11}.md\:col-span-12{grid-column:span 12 / span 12}.md\:col-span-2{grid-column:span 2 / span 2}.md\:col-span-3{grid-column:span 3 / span 3}.md\:col-span-4{grid-column:span 4 / span 4}.md\:col-span-5{grid-column:span 5 / span 5}.md\:col-span-6{grid-column:span 6 / span 6}.md\:col-span-7{grid-column:span 7 / span 7}.md\:col-span-8{grid-column:span 8 / span 8}.md\:col-span-9{grid-column:span 9 / span 9}.md\:col-span-full{grid-column:1 / -1}.md\:row-span-1{grid-row:span 1 / span 1}.md\:row-span-2{grid-row:span 2 / span 2}.md\:row-span-3{grid-row:span 3 / span 3}.md\:row-span-4{grid-row:span 4 / span 4}.md\:row-span-5{grid-row:span 5 / span 5}.md\:row-span-6{grid-row:span 6 / span 6}.md\:row-span-full{grid-row:1 / -1}.md\:h-0{height:0px}.md\:h-0\.5{height:.125rem}.md\:h-1{height:.25rem}.md\:h-1\.5{height:.375rem}.md\:h-1\/2{height:50%}.md\:h-1\/3{height:33.333333%}.md\:h-1\/4{height:25%}.md\:h-1\/5{height:20%}.md\:h-1\/6{height:16.666667%}.md\:h-10{height:2.5rem}.md\:h-11{height:2.75rem}.md\:h-12{height:3rem}.md\:h-128{height:32rem}.md\:h-14{height:3.5rem}.md\:h-16{height:4rem}.md\:h-2{height:.5rem}.md\:h-2\.5{height:.625rem}.md\:h-2\/3{height:66.666667%}.md\:h-2\/4{height:50%}.md\:h-2\/5{height:40%}.md\:h-2\/6{height:33.333333%}.md\:h-20{height:5rem}.md\:h-24{height:6rem}.md\:h-28{height:7rem}.md\:h-3{height:.75rem}.md\:h-3\.5{height:.875rem}.md\:h-3\/4{height:75%}.md\:h-3\/5{height:60%}.md\:h-3\/6{height:50%}.md\:h-32{height:8rem}.md\:h-36{height:9rem}.md\:h-4{height:1rem}.md\:h-4\/5{height:80%}.md\:h-4\/6{height:66.666667%}.md\:h-40{height:10rem}.md\:h-44{height:11rem}.md\:h-48{height:12rem}.md\:h-5{height:1.25rem}.md\:h-5\/6{height:83.333333%}.md\:h-52{height:13rem}.md\:h-56{height:14rem}.md\:h-6{height:1.5rem}.md\:h-60{height:15rem}.md\:h-64{height:16rem}.md\:h-7{height:1.75rem}.md\:h-72{height:18rem}.md\:h-8{height:2rem}.md\:h-80{height:20rem}.md\:h-9{height:2.25rem}.md\:h-96{height:24rem}.md\:max-h-0{max-height:0px}.md\:max-h-0\.5{max-height:.125rem}.md\:max-h-1{max-height:.25rem}.md\:max-h-1\.5{max-height:.375rem}.md\:max-h-10{max-height:2.5rem}.md\:max-h-11{max-height:2.75rem}.md\:max-h-12{max-height:3rem}.md\:max-h-14{max-height:3.5rem}.md\:max-h-16{max-height:4rem}.md\:max-h-2{max-height:.5rem}.md\:max-h-2\.5{max-height:.625rem}.md\:max-h-20{max-height:5rem}.md\:max-h-24{max-height:6rem}.md\:max-h-28{max-height:7rem}.md\:max-h-3{max-height:.75rem}.md\:max-h-3\.5{max-height:.875rem}.md\:max-h-32{max-height:8rem}.md\:max-h-36{max-height:9rem}.md\:max-h-4{max-height:1rem}.md\:max-h-40{max-height:10rem}.md\:max-h-44{max-height:11rem}.md\:max-h-48{max-height:12rem}.md\:max-h-5{max-height:1.25rem}.md\:max-h-52{max-height:13rem}.md\:max-h-56{max-height:14rem}.md\:max-h-6{max-height:1.5rem}.md\:max-h-60{max-height:15rem}.md\:max-h-64{max-height:16rem}.md\:max-h-7{max-height:1.75rem}.md\:max-h-72{max-height:18rem}.md\:max-h-8{max-height:2rem}.md\:max-h-80{max-height:20rem}.md\:max-h-9{max-height:2.25rem}.md\:max-h-96{max-height:24rem}.md\:min-h-0{min-height:0px}.md\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.md\:grid-cols-10{grid-template-columns:repeat(10,minmax(0,1fr))}.md\:grid-cols-11{grid-template-columns:repeat(11,minmax(0,1fr))}.md\:grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.md\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.md\:grid-cols-7{grid-template-columns:repeat(7,minmax(0,1fr))}.md\:grid-cols-8{grid-template-columns:repeat(8,minmax(0,1fr))}.md\:grid-cols-9{grid-template-columns:repeat(9,minmax(0,1fr))}}@media (min-width: 1024px){.lg\:col-span-1{grid-column:span 1 / span 1}.lg\:col-span-10{grid-column:span 10 / span 10}.lg\:col-span-11{grid-column:span 11 / span 11}.lg\:col-span-12{grid-column:span 12 / span 12}.lg\:col-span-2{grid-column:span 2 / span 2}.lg\:col-span-3{grid-column:span 3 / span 3}.lg\:col-span-4{grid-column:span 4 / span 4}.lg\:col-span-5{grid-column:span 5 / span 5}.lg\:col-span-6{grid-column:span 6 / span 6}.lg\:col-span-7{grid-column:span 7 / span 7}.lg\:col-span-8{grid-column:span 8 / span 8}.lg\:col-span-9{grid-column:span 9 / span 9}.lg\:col-span-full{grid-column:1 / -1}.lg\:row-span-1{grid-row:span 1 / span 1}.lg\:row-span-2{grid-row:span 2 / span 2}.lg\:row-span-3{grid-row:span 3 / span 3}.lg\:row-span-4{grid-row:span 4 / span 4}.lg\:row-span-5{grid-row:span 5 / span 5}.lg\:row-span-6{grid-row:span 6 / span 6}.lg\:row-span-full{grid-row:1 / -1}.lg\:h-0{height:0px}.lg\:h-0\.5{height:.125rem}.lg\:h-1{height:.25rem}.lg\:h-1\.5{height:.375rem}.lg\:h-1\/2{height:50%}.lg\:h-1\/3{height:33.333333%}.lg\:h-1\/4{height:25%}.lg\:h-1\/5{height:20%}.lg\:h-1\/6{height:16.666667%}.lg\:h-10{height:2.5rem}.lg\:h-11{height:2.75rem}.lg\:h-12{height:3rem}.lg\:h-128{height:32rem}.lg\:h-14{height:3.5rem}.lg\:h-16{height:4rem}.lg\:h-2{height:.5rem}.lg\:h-2\.5{height:.625rem}.lg\:h-2\/3{height:66.666667%}.lg\:h-2\/4{height:50%}.lg\:h-2\/5{height:40%}.lg\:h-2\/6{height:33.333333%}.lg\:h-20{height:5rem}.lg\:h-24{height:6rem}.lg\:h-28{height:7rem}.lg\:h-3{height:.75rem}.lg\:h-3\.5{height:.875rem}.lg\:h-3\/4{height:75%}.lg\:h-3\/5{height:60%}.lg\:h-3\/6{height:50%}.lg\:h-32{height:8rem}.lg\:h-36{height:9rem}.lg\:h-4{height:1rem}.lg\:h-4\/5{height:80%}.lg\:h-4\/6{height:66.666667%}.lg\:h-40{height:10rem}.lg\:h-44{height:11rem}.lg\:h-48{height:12rem}.lg\:h-5{height:1.25rem}.lg\:h-5\/6{height:83.333333%}.lg\:h-52{height:13rem}.lg\:h-56{height:14rem}.lg\:h-6{height:1.5rem}.lg\:h-60{height:15rem}.lg\:h-64{height:16rem}.lg\:h-7{height:1.75rem}.lg\:h-72{height:18rem}.lg\:h-8{height:2rem}.lg\:h-80{height:20rem}.lg\:h-9{height:2.25rem}.lg\:h-96{height:24rem}.lg\:max-h-0{max-height:0px}.lg\:max-h-0\.5{max-height:.125rem}.lg\:max-h-1{max-height:.25rem}.lg\:max-h-1\.5{max-height:.375rem}.lg\:max-h-10{max-height:2.5rem}.lg\:max-h-11{max-height:2.75rem}.lg\:max-h-12{max-height:3rem}.lg\:max-h-14{max-height:3.5rem}.lg\:max-h-16{max-height:4rem}.lg\:max-h-2{max-height:.5rem}.lg\:max-h-2\.5{max-height:.625rem}.lg\:max-h-20{max-height:5rem}.lg\:max-h-24{max-height:6rem}.lg\:max-h-28{max-height:7rem}.lg\:max-h-3{max-height:.75rem}.lg\:max-h-3\.5{max-height:.875rem}.lg\:max-h-32{max-height:8rem}.lg\:max-h-36{max-height:9rem}.lg\:max-h-4{max-height:1rem}.lg\:max-h-40{max-height:10rem}.lg\:max-h-44{max-height:11rem}.lg\:max-h-48{max-height:12rem}.lg\:max-h-5{max-height:1.25rem}.lg\:max-h-52{max-height:13rem}.lg\:max-h-56{max-height:14rem}.lg\:max-h-6{max-height:1.5rem}.lg\:max-h-60{max-height:15rem}.lg\:max-h-64{max-height:16rem}.lg\:max-h-7{max-height:1.75rem}.lg\:max-h-72{max-height:18rem}.lg\:max-h-8{max-height:2rem}.lg\:max-h-80{max-height:20rem}.lg\:max-h-9{max-height:2.25rem}.lg\:max-h-96{max-height:24rem}.lg\:min-h-0{min-height:0px}.lg\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.lg\:grid-cols-10{grid-template-columns:repeat(10,minmax(0,1fr))}.lg\:grid-cols-11{grid-template-columns:repeat(11,minmax(0,1fr))}.lg\:grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.lg\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.lg\:grid-cols-7{grid-template-columns:repeat(7,minmax(0,1fr))}.lg\:grid-cols-8{grid-template-columns:repeat(8,minmax(0,1fr))}.lg\:grid-cols-9{grid-template-columns:repeat(9,minmax(0,1fr))}html :where(.default\:lg\:col-span-1){grid-column:span 1 / span 1}html :where(.default\:lg\:col-span-10){grid-column:span 10 / span 10}html :where(.default\:lg\:col-span-11){grid-column:span 11 / span 11}html :where(.default\:lg\:col-span-12){grid-column:span 12 / span 12}html :where(.default\:lg\:col-span-2){grid-column:span 2 / span 2}html :where(.default\:lg\:col-span-3){grid-column:span 3 / span 3}html :where(.default\:lg\:col-span-4){grid-column:span 4 / span 4}html :where(.default\:lg\:col-span-5){grid-column:span 5 / span 5}html :where(.default\:lg\:col-span-6){grid-column:span 6 / span 6}html :where(.default\:lg\:col-span-7){grid-column:span 7 / span 7}html :where(.default\:lg\:col-span-8){grid-column:span 8 / span 8}html :where(.default\:lg\:col-span-9){grid-column:span 9 / span 9}html :where(.default\:lg\:col-span-full){grid-column:1 / -1}html :where(.default\:lg\:row-span-1){grid-row:span 1 / span 1}html :where(.default\:lg\:row-span-2){grid-row:span 2 / span 2}html :where(.default\:lg\:row-span-3){grid-row:span 3 / span 3}html :where(.default\:lg\:row-span-4){grid-row:span 4 / span 4}html :where(.default\:lg\:row-span-5){grid-row:span 5 / span 5}html :where(.default\:lg\:row-span-6){grid-row:span 6 / span 6}html :where(.default\:lg\:row-span-full){grid-row:1 / -1}html :where(.default\:lg\:grid-cols-1){grid-template-columns:repeat(1,minmax(0,1fr))}html :where(.default\:lg\:grid-cols-10){grid-template-columns:repeat(10,minmax(0,1fr))}html :where(.default\:lg\:grid-cols-11){grid-template-columns:repeat(11,minmax(0,1fr))}html :where(.default\:lg\:grid-cols-12){grid-template-columns:repeat(12,minmax(0,1fr))}html :where(.default\:lg\:grid-cols-2){grid-template-columns:repeat(2,minmax(0,1fr))}html :where(.default\:lg\:grid-cols-3){grid-template-columns:repeat(3,minmax(0,1fr))}html :where(.default\:lg\:grid-cols-4){grid-template-columns:repeat(4,minmax(0,1fr))}html :where(.default\:lg\:grid-cols-5){grid-template-columns:repeat(5,minmax(0,1fr))}html :where(.default\:lg\:grid-cols-6){grid-template-columns:repeat(6,minmax(0,1fr))}html :where(.default\:lg\:grid-cols-7){grid-template-columns:repeat(7,minmax(0,1fr))}html :where(.default\:lg\:grid-cols-8){grid-template-columns:repeat(8,minmax(0,1fr))}html :where(.default\:lg\:grid-cols-9){grid-template-columns:repeat(9,minmax(0,1fr))}}@media (min-width: 1280px){.xl\:col-span-1{grid-column:span 1 / span 1}.xl\:col-span-10{grid-column:span 10 / span 10}.xl\:col-span-11{grid-column:span 11 / span 11}.xl\:col-span-12{grid-column:span 12 / span 12}.xl\:col-span-2{grid-column:span 2 / span 2}.xl\:col-span-3{grid-column:span 3 / span 3}.xl\:col-span-4{grid-column:span 4 / span 4}.xl\:col-span-5{grid-column:span 5 / span 5}.xl\:col-span-6{grid-column:span 6 / span 6}.xl\:col-span-7{grid-column:span 7 / span 7}.xl\:col-span-8{grid-column:span 8 / span 8}.xl\:col-span-9{grid-column:span 9 / span 9}.xl\:col-span-full{grid-column:1 / -1}.xl\:row-span-1{grid-row:span 1 / span 1}.xl\:row-span-2{grid-row:span 2 / span 2}.xl\:row-span-3{grid-row:span 3 / span 3}.xl\:row-span-4{grid-row:span 4 / span 4}.xl\:row-span-5{grid-row:span 5 / span 5}.xl\:row-span-6{grid-row:span 6 / span 6}.xl\:row-span-full{grid-row:1 / -1}.xl\:h-0{height:0px}.xl\:h-0\.5{height:.125rem}.xl\:h-1{height:.25rem}.xl\:h-1\.5{height:.375rem}.xl\:h-1\/2{height:50%}.xl\:h-1\/3{height:33.333333%}.xl\:h-1\/4{height:25%}.xl\:h-1\/5{height:20%}.xl\:h-1\/6{height:16.666667%}.xl\:h-10{height:2.5rem}.xl\:h-11{height:2.75rem}.xl\:h-12{height:3rem}.xl\:h-128{height:32rem}.xl\:h-14{height:3.5rem}.xl\:h-16{height:4rem}.xl\:h-2{height:.5rem}.xl\:h-2\.5{height:.625rem}.xl\:h-2\/3{height:66.666667%}.xl\:h-2\/4{height:50%}.xl\:h-2\/5{height:40%}.xl\:h-2\/6{height:33.333333%}.xl\:h-20{height:5rem}.xl\:h-24{height:6rem}.xl\:h-28{height:7rem}.xl\:h-3{height:.75rem}.xl\:h-3\.5{height:.875rem}.xl\:h-3\/4{height:75%}.xl\:h-3\/5{height:60%}.xl\:h-3\/6{height:50%}.xl\:h-32{height:8rem}.xl\:h-36{height:9rem}.xl\:h-4{height:1rem}.xl\:h-4\/5{height:80%}.xl\:h-4\/6{height:66.666667%}.xl\:h-40{height:10rem}.xl\:h-44{height:11rem}.xl\:h-48{height:12rem}.xl\:h-5{height:1.25rem}.xl\:h-5\/6{height:83.333333%}.xl\:h-52{height:13rem}.xl\:h-56{height:14rem}.xl\:h-6{height:1.5rem}.xl\:h-60{height:15rem}.xl\:h-64{height:16rem}.xl\:h-7{height:1.75rem}.xl\:h-72{height:18rem}.xl\:h-8{height:2rem}.xl\:h-80{height:20rem}.xl\:h-9{height:2.25rem}.xl\:h-96{height:24rem}.xl\:max-h-0{max-height:0px}.xl\:max-h-0\.5{max-height:.125rem}.xl\:max-h-1{max-height:.25rem}.xl\:max-h-1\.5{max-height:.375rem}.xl\:max-h-10{max-height:2.5rem}.xl\:max-h-11{max-height:2.75rem}.xl\:max-h-12{max-height:3rem}.xl\:max-h-14{max-height:3.5rem}.xl\:max-h-16{max-height:4rem}.xl\:max-h-2{max-height:.5rem}.xl\:max-h-2\.5{max-height:.625rem}.xl\:max-h-20{max-height:5rem}.xl\:max-h-24{max-height:6rem}.xl\:max-h-28{max-height:7rem}.xl\:max-h-3{max-height:.75rem}.xl\:max-h-3\.5{max-height:.875rem}.xl\:max-h-32{max-height:8rem}.xl\:max-h-36{max-height:9rem}.xl\:max-h-4{max-height:1rem}.xl\:max-h-40{max-height:10rem}.xl\:max-h-44{max-height:11rem}.xl\:max-h-48{max-height:12rem}.xl\:max-h-5{max-height:1.25rem}.xl\:max-h-52{max-height:13rem}.xl\:max-h-56{max-height:14rem}.xl\:max-h-6{max-height:1.5rem}.xl\:max-h-60{max-height:15rem}.xl\:max-h-64{max-height:16rem}.xl\:max-h-7{max-height:1.75rem}.xl\:max-h-72{max-height:18rem}.xl\:max-h-8{max-height:2rem}.xl\:max-h-80{max-height:20rem}.xl\:max-h-9{max-height:2.25rem}.xl\:max-h-96{max-height:24rem}.xl\:min-h-0{min-height:0px}.xl\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.xl\:grid-cols-10{grid-template-columns:repeat(10,minmax(0,1fr))}.xl\:grid-cols-11{grid-template-columns:repeat(11,minmax(0,1fr))}.xl\:grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.xl\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.xl\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.xl\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.xl\:grid-cols-7{grid-template-columns:repeat(7,minmax(0,1fr))}.xl\:grid-cols-8{grid-template-columns:repeat(8,minmax(0,1fr))}.xl\:grid-cols-9{grid-template-columns:repeat(9,minmax(0,1fr))}.xl\:pr-12{padding-right:3rem}}@media (min-width: 1536px){.\32xl\:col-span-1{grid-column:span 1 / span 1}.\32xl\:col-span-10{grid-column:span 10 / span 10}.\32xl\:col-span-11{grid-column:span 11 / span 11}.\32xl\:col-span-12{grid-column:span 12 / span 12}.\32xl\:col-span-2{grid-column:span 2 / span 2}.\32xl\:col-span-3{grid-column:span 3 / span 3}.\32xl\:col-span-4{grid-column:span 4 / span 4}.\32xl\:col-span-5{grid-column:span 5 / span 5}.\32xl\:col-span-6{grid-column:span 6 / span 6}.\32xl\:col-span-7{grid-column:span 7 / span 7}.\32xl\:col-span-8{grid-column:span 8 / span 8}.\32xl\:col-span-9{grid-column:span 9 / span 9}.\32xl\:col-span-full{grid-column:1 / -1}.\32xl\:row-span-1{grid-row:span 1 / span 1}.\32xl\:row-span-2{grid-row:span 2 / span 2}.\32xl\:row-span-3{grid-row:span 3 / span 3}.\32xl\:row-span-4{grid-row:span 4 / span 4}.\32xl\:row-span-5{grid-row:span 5 / span 5}.\32xl\:row-span-6{grid-row:span 6 / span 6}.\32xl\:row-span-full{grid-row:1 / -1}.\32xl\:h-0{height:0px}.\32xl\:h-0\.5{height:.125rem}.\32xl\:h-1{height:.25rem}.\32xl\:h-1\.5{height:.375rem}.\32xl\:h-1\/2{height:50%}.\32xl\:h-1\/3{height:33.333333%}.\32xl\:h-1\/4{height:25%}.\32xl\:h-1\/5{height:20%}.\32xl\:h-1\/6{height:16.666667%}.\32xl\:h-10{height:2.5rem}.\32xl\:h-11{height:2.75rem}.\32xl\:h-12{height:3rem}.\32xl\:h-128{height:32rem}.\32xl\:h-14{height:3.5rem}.\32xl\:h-16{height:4rem}.\32xl\:h-2{height:.5rem}.\32xl\:h-2\.5{height:.625rem}.\32xl\:h-2\/3{height:66.666667%}.\32xl\:h-2\/4{height:50%}.\32xl\:h-2\/5{height:40%}.\32xl\:h-2\/6{height:33.333333%}.\32xl\:h-20{height:5rem}.\32xl\:h-24{height:6rem}.\32xl\:h-28{height:7rem}.\32xl\:h-3{height:.75rem}.\32xl\:h-3\.5{height:.875rem}.\32xl\:h-3\/4{height:75%}.\32xl\:h-3\/5{height:60%}.\32xl\:h-3\/6{height:50%}.\32xl\:h-32{height:8rem}.\32xl\:h-36{height:9rem}.\32xl\:h-4{height:1rem}.\32xl\:h-4\/5{height:80%}.\32xl\:h-4\/6{height:66.666667%}.\32xl\:h-40{height:10rem}.\32xl\:h-44{height:11rem}.\32xl\:h-48{height:12rem}.\32xl\:h-5{height:1.25rem}.\32xl\:h-5\/6{height:83.333333%}.\32xl\:h-52{height:13rem}.\32xl\:h-56{height:14rem}.\32xl\:h-6{height:1.5rem}.\32xl\:h-60{height:15rem}.\32xl\:h-64{height:16rem}.\32xl\:h-7{height:1.75rem}.\32xl\:h-72{height:18rem}.\32xl\:h-8{height:2rem}.\32xl\:h-80{height:20rem}.\32xl\:h-9{height:2.25rem}.\32xl\:h-96{height:24rem}.\32xl\:max-h-0{max-height:0px}.\32xl\:max-h-0\.5{max-height:.125rem}.\32xl\:max-h-1{max-height:.25rem}.\32xl\:max-h-1\.5{max-height:.375rem}.\32xl\:max-h-10{max-height:2.5rem}.\32xl\:max-h-11{max-height:2.75rem}.\32xl\:max-h-12{max-height:3rem}.\32xl\:max-h-14{max-height:3.5rem}.\32xl\:max-h-16{max-height:4rem}.\32xl\:max-h-2{max-height:.5rem}.\32xl\:max-h-2\.5{max-height:.625rem}.\32xl\:max-h-20{max-height:5rem}.\32xl\:max-h-24{max-height:6rem}.\32xl\:max-h-28{max-height:7rem}.\32xl\:max-h-3{max-height:.75rem}.\32xl\:max-h-3\.5{max-height:.875rem}.\32xl\:max-h-32{max-height:8rem}.\32xl\:max-h-36{max-height:9rem}.\32xl\:max-h-4{max-height:1rem}.\32xl\:max-h-40{max-height:10rem}.\32xl\:max-h-44{max-height:11rem}.\32xl\:max-h-48{max-height:12rem}.\32xl\:max-h-5{max-height:1.25rem}.\32xl\:max-h-52{max-height:13rem}.\32xl\:max-h-56{max-height:14rem}.\32xl\:max-h-6{max-height:1.5rem}.\32xl\:max-h-60{max-height:15rem}.\32xl\:max-h-64{max-height:16rem}.\32xl\:max-h-7{max-height:1.75rem}.\32xl\:max-h-72{max-height:18rem}.\32xl\:max-h-8{max-height:2rem}.\32xl\:max-h-80{max-height:20rem}.\32xl\:max-h-9{max-height:2.25rem}.\32xl\:max-h-96{max-height:24rem}.\32xl\:min-h-0{min-height:0px}.\32xl\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.\32xl\:grid-cols-10{grid-template-columns:repeat(10,minmax(0,1fr))}.\32xl\:grid-cols-11{grid-template-columns:repeat(11,minmax(0,1fr))}.\32xl\:grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.\32xl\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.\32xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.\32xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.\32xl\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.\32xl\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.\32xl\:grid-cols-7{grid-template-columns:repeat(7,minmax(0,1fr))}.\32xl\:grid-cols-8{grid-template-columns:repeat(8,minmax(0,1fr))}.\32xl\:grid-cols-9{grid-template-columns:repeat(9,minmax(0,1fr))}}.\[\&\>svg\]\:h-6>svg{height:1.5rem}.\[\&\>svg\]\:w-6>svg{width:1.5rem}.\[\&\>svg\]\:flex-shrink-0>svg{flex-shrink:0}.\[\&\>svg\]\:stroke-gray-400>svg{stroke:#9ca3af}:is(.dark .\[\&\>svg\]\:dark\:stroke-gray-600)>svg{stroke:#4b5563} diff --git a/package-lock.json b/package-lock.json index bd9f6c4c..83fdd0c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "@tailwindcss/forms": "^0.5.3", "autoprefixer": "^10.4.13", "chart.js": "^4.4.0", - "postcss": "^8.4.20", + "postcss": "^8.4.31", "tailwindcss": "^3.2.4", "vite": "^4.0.3" } @@ -1191,9 +1191,9 @@ } }, "node_modules/postcss": { - "version": "8.4.29", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.29.tgz", - "integrity": "sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "dev": true, "funding": [ { diff --git a/package.json b/package.json index f637870a..d194e8ca 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "@tailwindcss/forms": "^0.5.3", "autoprefixer": "^10.4.13", "chart.js": "^4.4.0", - "postcss": "^8.4.20", + "postcss": "^8.4.31", "tailwindcss": "^3.2.4", "vite": "^4.0.3" } From 08559ae57e5132f2618f9334df806af5416a9a3d Mon Sep 17 00:00:00 2001 From: Dries Vints Date: Wed, 6 Dec 2023 14:21:53 +0100 Subject: [PATCH 040/110] Require Livewire version in issue template (#161) * Require Livewire version in issue template * Update 1_Bug_report.yml Co-authored-by: Julius Kiekbusch --------- Co-authored-by: James Brooks Co-authored-by: Julius Kiekbusch --- .github/ISSUE_TEMPLATE/1_Bug_report.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/1_Bug_report.yml b/.github/ISSUE_TEMPLATE/1_Bug_report.yml index ebfdb3ef..d288f66c 100644 --- a/.github/ISSUE_TEMPLATE/1_Bug_report.yml +++ b/.github/ISSUE_TEMPLATE/1_Bug_report.yml @@ -25,6 +25,13 @@ body: placeholder: 8.1.4 validations: required: true + - type: input + attributes: + label: Livewire Version + description: Provide the Livewire version that you are using. + placeholder: 3.0.2 + validations: + required: true - type: input attributes: label: Database Driver & Version From 6404f902a2244b4f6dcb9de0f0694e3b0b4f7278 Mon Sep 17 00:00:00 2001 From: Jess Archer Date: Thu, 7 Dec 2023 01:00:49 +1000 Subject: [PATCH 041/110] [1.x] Custom card CSS (#157) * Allow cards to include custom CSS * Fix code styling * Allow Htmlable CSS * Fix code styling * Add tests * Fix code styling --------- Co-authored-by: jessarcher --- resources/views/components/pulse.blade.php | 8 +--- src/Facades/Pulse.php | 2 +- src/Livewire/Card.php | 26 ++++++++++ src/Pulse.php | 36 +++++++++++--- tests/Feature/Livewire/CustomCardTest.php | 55 ++++++++++++++++++++++ tests/fixtures/custom.css | 3 ++ 6 files changed, 117 insertions(+), 13 deletions(-) create mode 100644 tests/Feature/Livewire/CustomCardTest.php create mode 100644 tests/fixtures/custom.css diff --git a/resources/views/components/pulse.blade.php b/resources/views/components/pulse.blade.php index b3930042..4a2c3458 100644 --- a/resources/views/components/pulse.blade.php +++ b/resources/views/components/pulse.blade.php @@ -11,15 +11,11 @@ - + {!! Laravel\Pulse\Facades\Pulse::css() !!} @livewireStyles - + {!! Laravel\Pulse\Facades\Pulse::js() !!}
diff --git a/src/Facades/Pulse.php b/src/Facades/Pulse.php index 6471aaa4..78c93e8b 100644 --- a/src/Facades/Pulse.php +++ b/src/Facades/Pulse.php @@ -24,7 +24,7 @@ * @method static \Laravel\Pulse\Pulse resolveAuthenticatedUserIdUsing(callable $callback) * @method static mixed|null withUser(\Illuminate\Contracts\Auth\Authenticatable|string|int|null $user, callable $callback) * @method static \Laravel\Pulse\Pulse rememberUser(\Illuminate\Contracts\Auth\Authenticatable $user) - * @method static string css() + * @method static string|self css(array|string|\Illuminate\Contracts\Support\Htmlable|null $path = null) * @method static string js() * @method static bool registersRoutes() * @method static \Laravel\Pulse\Pulse ignoreRoutes() diff --git a/src/Livewire/Card.php b/src/Livewire/Card.php index bc284df5..3b3036a4 100644 --- a/src/Livewire/Card.php +++ b/src/Livewire/Card.php @@ -4,7 +4,9 @@ use Illuminate\Contracts\Support\Renderable; use Illuminate\Support\Facades\View; +use Laravel\Pulse\Facades\Pulse; use Livewire\Component; +use Livewire\Livewire; abstract class Card extends Component { @@ -43,4 +45,28 @@ public function placeholder(): Renderable 'class' => $this->class, ]); } + + /** + * Capture component-specific CSS. + * + * @return void + */ + public function dehydrate() + { + if (Livewire::isLivewireRequest()) { + return; + } + + Pulse::css($this->css()); + } + + /** + * Define any CSS that should be loaded for the component. + * + * @return string|\Illuminate\Contracts\Support\Htmlable|array|null + */ + protected function css() + { + return null; + } } diff --git a/src/Pulse.php b/src/Pulse.php index 696bb721..9983e6be 100644 --- a/src/Pulse.php +++ b/src/Pulse.php @@ -7,8 +7,11 @@ use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Contracts\Foundation\Application; +use Illuminate\Contracts\Support\Htmlable; +use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\Lottery; +use Illuminate\Support\Str; use Illuminate\Support\Traits\ForwardsCalls; use Laravel\Pulse\Contracts\Ingest; use Laravel\Pulse\Contracts\Storage; @@ -89,6 +92,13 @@ class Pulse */ protected $handleExceptionsUsing = null; + /** + * The CSS paths to include on the dashboard. + * + * @var list + */ + protected $css = [__DIR__.'/../dist/pulse.css']; + /** * Create a new Pulse instance. */ @@ -428,15 +438,29 @@ public function rememberUser(Authenticatable $user): self } /** - * Return the compiled CSS from the vendor directory. + * Register or return CSS for the Pulse dashboard. + * + * @param string|Htmlable|list|null $css */ - public function css(): string + public function css(string|Htmlable|array|null $css = null): string|self { - if (($content = file_get_contents(__DIR__.'/../dist/pulse.css')) === false) { - throw new RuntimeException('Unable to load Pulse dashboard CSS.'); + if (func_num_args() === 1) { + $this->css = array_values(array_unique(array_merge($this->css, Arr::wrap($css)))); + + return $this; } - return $content; + return collect($this->css)->reduce(function ($carry, $css) { + if ($css instanceof Htmlable) { + return $carry.Str::finish($css->toHtml(), PHP_EOL); + } else { + if (($contents = @file_get_contents($css)) === false) { + throw new RuntimeException("Unable to load Pulse dashboard CSS path [$css]."); + } + + return $carry."".PHP_EOL; + } + }, ''); } /** @@ -448,7 +472,7 @@ public function js(): string throw new RuntimeException('Unable to load the Pulse dashboard JavaScript.'); } - return $content; + return "".PHP_EOL; } /** diff --git a/tests/Feature/Livewire/CustomCardTest.php b/tests/Feature/Livewire/CustomCardTest.php new file mode 100644 index 00000000..d17b0177 --- /dev/null +++ b/tests/Feature/Livewire/CustomCardTest.php @@ -0,0 +1,55 @@ +assertOk(); + + $css = Pulse::css(); + + expect($css)->toContain(<<<'HTML' + + HTML); +}); + +it('loads custom css using a Htmlable', function () { + Livewire::test(CustomCardWithCssHtmlable::class) + ->assertOk(); + + $css = Pulse::css(); + + expect($css)->toContain(''); +}); + +class CustomCardWithCssPath extends Card +{ + public function render() + { + return '
'; + } + + protected function css() + { + return __DIR__.'/../../fixtures/custom.css'; + } +} + +class CustomCardWithCssHtmlable extends Card +{ + public function render() + { + return '
'; + } + + protected function css() + { + return new HtmlString(''); + } +} diff --git a/tests/fixtures/custom.css b/tests/fixtures/custom.css new file mode 100644 index 00000000..1f3aaeb2 --- /dev/null +++ b/tests/fixtures/custom.css @@ -0,0 +1,3 @@ +.custom-class { + color: purple; +} From 77b2de23992a2b2d37ededd957aebb131c896114 Mon Sep 17 00:00:00 2001 From: taylorotwell Date: Wed, 6 Dec 2023 15:01:16 +0000 Subject: [PATCH 042/110] Update facade docblocks --- src/Facades/Pulse.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Facades/Pulse.php b/src/Facades/Pulse.php index 78c93e8b..c8519678 100644 --- a/src/Facades/Pulse.php +++ b/src/Facades/Pulse.php @@ -24,7 +24,7 @@ * @method static \Laravel\Pulse\Pulse resolveAuthenticatedUserIdUsing(callable $callback) * @method static mixed|null withUser(\Illuminate\Contracts\Auth\Authenticatable|string|int|null $user, callable $callback) * @method static \Laravel\Pulse\Pulse rememberUser(\Illuminate\Contracts\Auth\Authenticatable $user) - * @method static string|self css(array|string|\Illuminate\Contracts\Support\Htmlable|null $path = null) + * @method static \Laravel\Pulse\Pulse|string css(string|\Illuminate\Contracts\Support\Htmlable|array|null $css = null) * @method static string js() * @method static bool registersRoutes() * @method static \Laravel\Pulse\Pulse ignoreRoutes() From 5f63272b8d864475a061e0963cd66e53c78d5f22 Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Thu, 7 Dec 2023 12:26:11 +1100 Subject: [PATCH 043/110] Fix number rounding (#159) --- resources/views/livewire/cache.blade.php | 4 ++-- tests/Feature/Livewire/CacheTest.php | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/resources/views/livewire/cache.blade.php b/resources/views/livewire/cache.blade.php index fcbce41d..b5dfdb4f 100644 --- a/resources/views/livewire/cache.blade.php +++ b/resources/views/livewire/cache.blade.php @@ -58,7 +58,7 @@
- {{ round($allCacheInteractions->hits / ($allCacheInteractions->hits + $allCacheInteractions->misses) * 100, 2).'%' }} + {{ ((int) ($allCacheInteractions->hits / ($allCacheInteractions->hits + $allCacheInteractions->misses) * 10000)) / 100 }}% Hit Rate @@ -105,7 +105,7 @@ @endif - {{ round($interaction->hits / ($interaction->hits + $interaction->misses) * 100, 2).'%' }} + {{ ((int) ($interaction->hits / ($interaction->hits + $interaction->misses) * 10000)) / 100 }}% @endforeach diff --git a/tests/Feature/Livewire/CacheTest.php b/tests/Feature/Livewire/CacheTest.php index 0908c9b7..65fac67d 100644 --- a/tests/Feature/Livewire/CacheTest.php +++ b/tests/Feature/Livewire/CacheTest.php @@ -49,3 +49,25 @@ (object) ['key' => 'bar', 'hits' => 2, 'misses' => 2], ])); }); + +it('does not round numbers up', function () { + for ($i = 0; $i < 20_000; $i++) { + Pulse::record('cache_hit', 'foo')->count()->onlyBuckets(); + } + Pulse::record('cache_miss', 'foo')->count()->onlyBuckets(); + Pulse::store(); + + Livewire::test(Cache::class, ['lazy' => false]) + ->assertDontSeeHtml("100.00%\n") + ->assertSeeHtml("99.99%\n"); +}); + +it('does not show decimals for round numbers', function () { + Pulse::record('cache_hit', 'foo')->count()->onlyBuckets(); + Pulse::record('cache_miss', 'foo')->count()->onlyBuckets(); + Pulse::store(); + + Livewire::test(Cache::class, ['lazy' => false]) + ->assertDontSeeHtml("50.00%\n") + ->assertSeeHtml("50%\n"); +}); From a9a61b955ed39c5c355d59486cdd5e82e8b8ef47 Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Fri, 8 Dec 2023 06:27:26 +1100 Subject: [PATCH 044/110] Ensure values always contain an integer (#171) --- src/Livewire/Cache.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Livewire/Cache.php b/src/Livewire/Cache.php index eebe8cfa..0509d89d 100644 --- a/src/Livewire/Cache.php +++ b/src/Livewire/Cache.php @@ -42,8 +42,8 @@ public function render(): Renderable ->map(function ($row) { return (object) [ 'key' => $row->key, - 'hits' => $row->cache_hit, - 'misses' => $row->cache_miss, + 'hits' => $row->cache_hit ?? 0, + 'misses' => $row->cache_miss ?? 0, ]; }), 'keys' From c6b933ebbf6bb68f3e35ee3549b2208ed7856883 Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Fri, 8 Dec 2023 06:29:09 +1100 Subject: [PATCH 045/110] Add the ability to disable SQL highlighting (#172) --- config/pulse.php | 1 + .../views/livewire/slow-queries.blade.php | 26 ++++++++++--------- src/Livewire/SlowQueries.php | 5 +++- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/config/pulse.php b/config/pulse.php index ed66d707..d998642b 100644 --- a/config/pulse.php +++ b/config/pulse.php @@ -194,6 +194,7 @@ 'sample_rate' => env('PULSE_SLOW_QUERIES_SAMPLE_RATE', 1), 'threshold' => env('PULSE_SLOW_QUERIES_THRESHOLD', 1000), 'location' => env('PULSE_SLOW_QUERIES_LOCATION', true), + 'highlighting' => env('PULSE_SLOW_QUERIES_HIGHLIGHTING', true), 'ignore' => [ '/(["`])pulse_[\w]+?\1/', // Pulse tables... ], diff --git a/resources/views/livewire/slow-queries.blade.php b/resources/views/livewire/slow-queries.blade.php index 299db5e2..b157a570 100644 --- a/resources/views/livewire/slow-queries.blade.php +++ b/resources/views/livewire/slow-queries.blade.php @@ -2,17 +2,19 @@ use \Doctrine\SqlFormatter\HtmlHighlighter; use \Doctrine\SqlFormatter\SqlFormatter; -$sqlFormatter = new SqlFormatter(new HtmlHighlighter([ - HtmlHighlighter::HIGHLIGHT_RESERVED => 'class="font-semibold"', - HtmlHighlighter::HIGHLIGHT_QUOTE => 'class="text-purple-200"', - HtmlHighlighter::HIGHLIGHT_BACKTICK_QUOTE => 'class="text-purple-200"', - HtmlHighlighter::HIGHLIGHT_BOUNDARY => 'class="text-cyan-200"', - HtmlHighlighter::HIGHLIGHT_NUMBER => 'class="text-orange-200"', - HtmlHighlighter::HIGHLIGHT_WORD => 'class="text-orange-200"', - HtmlHighlighter::HIGHLIGHT_VARIABLE => 'class="text-orange-200"', - HtmlHighlighter::HIGHLIGHT_ERROR => 'class="text-red-200"', - HtmlHighlighter::HIGHLIGHT_COMMENT => 'class="text-gray-400"', -], false)); +if ($config['highlighting']) { + $sqlFormatter = new SqlFormatter(new HtmlHighlighter([ + HtmlHighlighter::HIGHLIGHT_RESERVED => 'class="font-semibold"', + HtmlHighlighter::HIGHLIGHT_QUOTE => 'class="text-purple-200"', + HtmlHighlighter::HIGHLIGHT_BACKTICK_QUOTE => 'class="text-purple-200"', + HtmlHighlighter::HIGHLIGHT_BOUNDARY => 'class="text-cyan-200"', + HtmlHighlighter::HIGHLIGHT_NUMBER => 'class="text-orange-200"', + HtmlHighlighter::HIGHLIGHT_WORD => 'class="text-orange-200"', + HtmlHighlighter::HIGHLIGHT_VARIABLE => 'class="text-orange-200"', + HtmlHighlighter::HIGHLIGHT_ERROR => 'class="text-red-200"', + HtmlHighlighter::HIGHLIGHT_COMMENT => 'class="text-gray-400"', + ], false)); +} @endphp
- {!! $sqlFormatter->highlight($query->sql) !!} + {!! $config['highlighting'] ? $sqlFormatter->highlight($query->sql) : $query->sql !!} @if ($query->location)

{{ $query->location }} diff --git a/src/Livewire/SlowQueries.php b/src/Livewire/SlowQueries.php index 8e504444..41773c24 100644 --- a/src/Livewire/SlowQueries.php +++ b/src/Livewire/SlowQueries.php @@ -56,7 +56,10 @@ public function render(): Renderable return View::make('pulse::livewire.slow-queries', [ 'time' => $time, 'runAt' => $runAt, - 'config' => Config::get('pulse.recorders.'.SlowQueriesRecorder::class), + 'config' => [ + 'highlighting' => true, + ...Config::get('pulse.recorders.'.SlowQueriesRecorder::class), + ], 'slowQueries' => $slowQueries, ]); } From 16ad1312e0c579e083bde29732c03f006e090254 Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Fri, 8 Dec 2023 06:30:21 +1100 Subject: [PATCH 046/110] Resolve the user key via `getKey` (#169) --- src/Pulse.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Pulse.php b/src/Pulse.php index 9983e6be..4c53f358 100644 --- a/src/Pulse.php +++ b/src/Pulse.php @@ -338,10 +338,14 @@ public function resolveUsers(Collection $ids): Collection { if ($this->usersResolver) { return collect(($this->usersResolver)($ids)); - } elseif (class_exists(\App\Models\User::class)) { - return \App\Models\User::whereKey($ids)->get(['id', 'name', 'email']); - } elseif (class_exists(\App\User::class)) { - return \App\User::whereKey($ids)->get(['id', 'name', 'email']); + } + + if (class_exists($class = \App\Models\User::class) || class_exists($class = \App\User::class)) { + return $class::whereKey($ids)->get()->map(fn ($user) => [ + 'id' => $user->getKey(), + 'name' => $user->name, + 'email' => $user->email, + ]); } return $ids->map(fn (string|int $id) => [ From 2c97b11a10a9c667a1130b86f97eea5a6e0805e9 Mon Sep 17 00:00:00 2001 From: MK Date: Thu, 7 Dec 2023 14:35:39 -0500 Subject: [PATCH 047/110] [1.x] Collect server metrics on Windows (#165) * Collect server metrics on Windows * Fix static analysis complaint --- src/Recorders/Servers.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Recorders/Servers.php b/src/Recorders/Servers.php index 52c47603..84557f82 100644 --- a/src/Recorders/Servers.php +++ b/src/Recorders/Servers.php @@ -45,18 +45,21 @@ public function record(SharedBeat $event): void $memoryTotal = match (PHP_OS_FAMILY) { 'Darwin' => intval(`sysctl hw.memsize | grep -Eo '[0-9]+'` / 1024 / 1024), 'Linux' => intval(`cat /proc/meminfo | grep MemTotal | grep -E -o '[0-9]+'` / 1024), + 'Windows' => intval(((int) trim(`wmic ComputerSystem get TotalPhysicalMemory | more +1`)) / 1024 / 1024), default => throw new RuntimeException('The pulse:check command does not currently support '.PHP_OS_FAMILY), }; $memoryUsed = match (PHP_OS_FAMILY) { 'Darwin' => $memoryTotal - intval(intval(`vm_stat | grep 'Pages free' | grep -Eo '[0-9]+'`) * intval(`pagesize`) / 1024 / 1024), // MB 'Linux' => $memoryTotal - intval(`cat /proc/meminfo | grep MemAvailable | grep -E -o '[0-9]+'` / 1024), // MB + 'Windows' => $memoryTotal - intval(((int) trim(`wmic OS get FreePhysicalMemory | more +1`)) / 1024), // MB default => throw new RuntimeException('The pulse:check command does not currently support '.PHP_OS_FAMILY), }; $cpu = match (PHP_OS_FAMILY) { 'Darwin' => (int) `top -l 1 | grep -E "^CPU" | tail -1 | awk '{ print $3 + $5 }'`, 'Linux' => (int) `top -bn1 | grep '%Cpu(s)' | tail -1 | grep -Eo '[0-9]+\.[0-9]+' | head -n 4 | tail -1 | awk '{ print 100 - $1 }'`, + 'Windows' => (int) trim(`wmic cpu get loadpercentage | more +1`), default => throw new RuntimeException('The pulse:check command does not currently support '.PHP_OS_FAMILY), }; From 3936f1acbf1f2131591ec2ca5e2eef724b362cb9 Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Fri, 8 Dec 2023 06:36:09 +1100 Subject: [PATCH 048/110] [1.x] Improve redis exception (#168) * improve redis exception * Fix code styling --------- Co-authored-by: timacdonald --- src/Support/RedisAdapter.php | 6 +++--- src/Support/RedisClientException.php | 10 ---------- src/Support/RedisServerException.php | 26 ++++++++++++++++++++++++++ tests/Feature/RedisTest.php | 8 ++++++-- 4 files changed, 35 insertions(+), 15 deletions(-) delete mode 100644 src/Support/RedisClientException.php create mode 100644 src/Support/RedisServerException.php diff --git a/src/Support/RedisAdapter.php b/src/Support/RedisAdapter.php index 25d22495..d4dabd14 100644 --- a/src/Support/RedisAdapter.php +++ b/src/Support/RedisAdapter.php @@ -112,13 +112,13 @@ public function pipeline(callable $closure): array protected function handle(array $args): mixed { try { - return tap($this->run($args), function ($result) { + return tap($this->run($args), function ($result) use ($args) { if ($result === false && $this->client() instanceof PhpRedis) { - throw new RedisClientException($this->client()->getLastError() ?? 'An unknown error occurred.'); + throw RedisServerException::whileRunningCommand(implode(' ', $args), $this->client()->getLastError() ?? 'An unknown error occurred.'); } }); } catch (PredisServerException $e) { - throw new RedisClientException($e->getMessage(), previous: $e); + throw RedisServerException::whileRunningCommand(implode(' ', $args), $e->getMessage(), previous: $e); } } diff --git a/src/Support/RedisClientException.php b/src/Support/RedisClientException.php deleted file mode 100644 index 84ac27e4..00000000 --- a/src/Support/RedisClientException.php +++ /dev/null @@ -1,10 +0,0 @@ -xtrim('stream-name', 'FOO', 'a', 'xyz'); -})->with(['predis', 'phpredis'])->throws(RedisClientException::class, 'ERR syntax error'); +})->with(['predis', 'phpredis'])->throws(RedisServerException::class, 'The Redis version does not support the command or some of its arguments [XTRIM laravel_database_stream-name FOO a xyz]. Redis error: [ERR syntax error].'); + +it('prepends the error message with the run command', function () { + throw RedisServerException::whileRunningCommand('FOO BAR', 'Something happened'); +})->throws(RedisServerException::class, 'Error running command [FOO BAR]. Redis error: [Something happened].'); From a18af828b8f5f1447d1f48cedb953482fd928554 Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Fri, 8 Dec 2023 14:06:15 +1100 Subject: [PATCH 049/110] Improve test performance --- tests/Feature/Recorders/QueuesTest.php | 38 ++++++++++++------------ tests/Feature/Recorders/SlowJobsTest.php | 12 ++++---- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/tests/Feature/Recorders/QueuesTest.php b/tests/Feature/Recorders/QueuesTest.php index 97a9f627..11101fab 100644 --- a/tests/Feature/Recorders/QueuesTest.php +++ b/tests/Feature/Recorders/QueuesTest.php @@ -70,7 +70,7 @@ function queueAggregates() /* * Work the job for the first time. */ - Artisan::call('queue:work', ['--max-jobs' => 1, '--tries' => 2, '--stop-when-empty' => true]); + Artisan::call('queue:work', ['--max-jobs' => 1, '--tries' => 2, '--stop-when-empty' => true, '--sleep' => 0]); expect(Queue::size())->toBe(1); $aggregates = queueAggregates(); expect($aggregates)->toHaveCount(12); @@ -84,7 +84,7 @@ function queueAggregates() /* * Work the job for the second time. */ - Artisan::call('queue:work', ['--max-jobs' => 1, '--tries' => 2, '--stop-when-empty' => true]); + Artisan::call('queue:work', ['--max-jobs' => 1, '--tries' => 2, '--stop-when-empty' => true, '--sleep' => 0]); expect(Queue::size())->toBe(0); $aggregates = queueAggregates(); @@ -141,7 +141,7 @@ function queueAggregates() /* * Work the job for the first time. */ - Artisan::call('queue:work', ['--max-jobs' => 1, '--tries' => 2, '--stop-when-empty' => true]); + Artisan::call('queue:work', ['--max-jobs' => 1, '--tries' => 2, '--stop-when-empty' => true, '--sleep' => 0]); expect(Queue::size())->toBe(1); $aggregates = queueAggregates(); expect($aggregates)->toHaveCount(12); @@ -155,7 +155,7 @@ function queueAggregates() /* * Work the job for the second time. */ - Artisan::call('queue:work', ['--max-jobs' => 1, '--tries' => 2, '--stop-when-empty' => true]); + Artisan::call('queue:work', ['--max-jobs' => 1, '--tries' => 2, '--stop-when-empty' => true, '--sleep' => 0]); expect(Queue::size())->toBe(0); $aggregates = queueAggregates(); expect($aggregates)->toHaveCount(16); @@ -194,7 +194,7 @@ function queueAggregates() /* * Work the job for the first time. */ - Artisan::call('queue:work', ['--max-jobs' => 1, '--tries' => 2, '--stop-when-empty' => true]); + Artisan::call('queue:work', ['--max-jobs' => 1, '--tries' => 2, '--stop-when-empty' => true, '--sleep' => 0]); expect(Queue::size())->toBe(1); $aggregates = queueAggregates(); @@ -209,7 +209,7 @@ function queueAggregates() /* * Work the job for the second time. */ - Artisan::call('queue:work', ['--max-jobs' => 1, '--tries' => 2, '--stop-when-empty' => true]); + Artisan::call('queue:work', ['--max-jobs' => 1, '--tries' => 2, '--stop-when-empty' => true, '--sleep' => 0]); expect(Queue::size())->toBe(0); $aggregates = queueAggregates(); expect($aggregates)->toHaveCount(16); @@ -248,7 +248,7 @@ function queueAggregates() /* * Work the job for the first time. */ - Artisan::call('queue:work', ['--max-jobs' => 1, '--tries' => 2, '--stop-when-empty' => true]); + Artisan::call('queue:work', ['--max-jobs' => 1, '--tries' => 2, '--stop-when-empty' => true, '--sleep' => 0]); expect(Queue::size())->toBe(1); $aggregates = queueAggregates(); expect($aggregates)->toHaveCount(12); @@ -262,7 +262,7 @@ function queueAggregates() /* * Work the job for the second time. */ - Artisan::call('queue:work', ['--max-jobs' => 1, '--tries' => 2, '--stop-when-empty' => true]); + Artisan::call('queue:work', ['--max-jobs' => 1, '--tries' => 2, '--stop-when-empty' => true, '--sleep' => 0]); expect(Queue::size())->toBe(0); $aggregates = queueAggregates(); expect($aggregates)->toHaveCount(16); @@ -301,7 +301,7 @@ function queueAggregates() /* * Work the job for the first time. */ - Artisan::call('queue:work', ['--max-jobs' => 1, '--tries' => 2, '--stop-when-empty' => true]); + Artisan::call('queue:work', ['--max-jobs' => 1, '--tries' => 2, '--stop-when-empty' => true, '--sleep' => 0]); expect(Queue::size())->toBe(1); $aggregates = queueAggregates(); expect($aggregates)->toHaveCount(12); @@ -315,7 +315,7 @@ function queueAggregates() /* * Work the job for the second time. */ - Artisan::call('queue:work', ['--max-jobs' => 1, '--tries' => 2, '--stop-when-empty' => true]); + Artisan::call('queue:work', ['--max-jobs' => 1, '--tries' => 2, '--stop-when-empty' => true, '--sleep' => 0]); expect(Queue::size())->toBe(0); $aggregates = queueAggregates(); expect($aggregates)->toHaveCount(16); @@ -356,7 +356,7 @@ function queueAggregates() * Work the job for the first time. */ - Artisan::call('queue:work', ['--max-jobs' => 1, '--stop-when-empty' => true]); + Artisan::call('queue:work', ['--max-jobs' => 1, '--stop-when-empty' => true, '--sleep' => 0]); expect(Queue::size())->toBe(1); $aggregates = queueAggregates(); expect($aggregates)->toHaveCount(12); @@ -371,7 +371,7 @@ function queueAggregates() * Work the job for the second time. */ - Artisan::call('queue:work', ['--max-jobs' => 1, '--stop-when-empty' => true]); + Artisan::call('queue:work', ['--max-jobs' => 1, '--stop-when-empty' => true, '--sleep' => 0]); expect(Queue::size())->toBe(1); $aggregates = queueAggregates(); expect($aggregates)->toHaveCount(12); @@ -392,7 +392,7 @@ function queueAggregates() * Work the job for the third time. */ - Artisan::call('queue:work', ['--max-jobs' => 1, '--stop-when-empty' => true]); + Artisan::call('queue:work', ['--max-jobs' => 1, '--stop-when-empty' => true, '--sleep' => 0]); expect(Queue::size())->toBe(0); $aggregates = queueAggregates(); expect($aggregates)->toHaveCount(16); @@ -439,7 +439,7 @@ function queueAggregates() * Work the job for the first time. */ - Artisan::call('queue:work', ['--max-jobs' => 1, '--stop-when-empty' => true]); + Artisan::call('queue:work', ['--max-jobs' => 1, '--stop-when-empty' => true, '--sleep' => 0]); expect(Queue::size())->toBe(1); $aggregates = queueAggregates(); expect($aggregates)->toHaveCount(12); @@ -454,7 +454,7 @@ function queueAggregates() * Work the job for the second time. */ - Artisan::call('queue:work', ['--max-jobs' => 1, '--stop-when-empty' => true]); + Artisan::call('queue:work', ['--max-jobs' => 1, '--stop-when-empty' => true, '--sleep' => 0]); expect(Queue::size())->toBe(0); $aggregates = queueAggregates(); expect($aggregates)->toHaveCount(16); @@ -496,7 +496,7 @@ function queueAggregates() */ app(ExceptionHandler::class)->reportable(fn (\Throwable $e) => throw $e); - Artisan::call('queue:work', ['--max-jobs' => 1, '--stop-when-empty' => true]); + Artisan::call('queue:work', ['--max-jobs' => 1, '--stop-when-empty' => true, '--sleep' => 0]); app()->forgetInstance(ExceptionHandler::class); expect(Queue::size())->toBe(0); $aggregates = queueAggregates(); @@ -522,7 +522,7 @@ function queueAggregates() * Work the job for the first time. */ - Artisan::call('queue:work', ['--max-jobs' => 1, '--stop-when-empty' => true]); + Artisan::call('queue:work', ['--max-jobs' => 1, '--stop-when-empty' => true, '--sleep' => 0]); expect(Queue::size())->toBe(1); expect(queueAggregates())->toHaveCount(0); @@ -530,7 +530,7 @@ function queueAggregates() * Work the job for the second time. */ - Artisan::call('queue:work', ['--max-jobs' => 1, '--stop-when-empty' => true]); + Artisan::call('queue:work', ['--max-jobs' => 1, '--stop-when-empty' => true, '--sleep' => 0]); expect(Queue::size())->toBe(0); expect(queueAggregates())->toHaveCount(0); }); @@ -615,7 +615,7 @@ function queueAggregates() value: 1, ); - Artisan::call('queue:work', ['--tries' => 2, '--max-jobs' => 4, '--stop-when-empty' => true]); + Artisan::call('queue:work', ['--tries' => 2, '--max-jobs' => 4, '--stop-when-empty' => true, '--sleep' => 0]); expect(Queue::size())->toBe(0); $aggregates = queueAggregates(); expect($aggregates)->toHaveCount(16); diff --git a/tests/Feature/Recorders/SlowJobsTest.php b/tests/Feature/Recorders/SlowJobsTest.php index 1edead48..928c4ef2 100644 --- a/tests/Feature/Recorders/SlowJobsTest.php +++ b/tests/Feature/Recorders/SlowJobsTest.php @@ -32,7 +32,7 @@ */ Carbon::setTestNow('2000-01-02 03:04:10'); - Artisan::call('queue:work', ['--max-jobs' => 1, '--stop-when-empty' => true]); + Artisan::call('queue:work', ['--max-jobs' => 1, '--stop-when-empty' => true, '--sleep' => 0]); expect(Queue::size())->toBe(0); $entries = Pulse::ignore(fn () => DB::table('pulse_entries')->where('type', 'slow_job')->get()); expect($entries)->toHaveCount(1); @@ -79,7 +79,7 @@ */ Carbon::setTestNow('2000-01-02 03:04:10'); - Artisan::call('queue:work', ['--max-jobs' => 1, '--stop-when-empty' => true]); + Artisan::call('queue:work', ['--max-jobs' => 1, '--stop-when-empty' => true, '--sleep' => 0]); expect(Queue::size())->toBe(0); expect(Pulse::ignore(fn () => DB::table('pulse_entries')->where('type', 'slow_job')->get()))->toHaveCount(0); expect(Pulse::ignore(fn () => DB::table('pulse_aggregates')->where('type', 'slow_job')->get()))->toHaveCount(0); @@ -106,7 +106,7 @@ * Work the job. */ - Artisan::call('queue:work', ['--max-jobs' => 1, '--stop-when-empty' => true]); + Artisan::call('queue:work', ['--max-jobs' => 1, '--stop-when-empty' => true, '--sleep' => 0]); expect(Queue::size())->toBe(0); expect(Pulse::ignore(fn () => DB::table('pulse_entries')->where('type', 'slow_job')->get()))->toHaveCount(0); @@ -140,7 +140,7 @@ * Work the jobs. */ - Artisan::call('queue:work', ['--stop-when-empty' => true]); + Artisan::call('queue:work', ['--stop-when-empty' => true, '--sleep' => 0]); expect(Queue::size())->toBe(0); expect(Pulse::ignore(fn () => DB::table('pulse_entries')->where('type', 'slow_job')->count()))->toEqualWithDelta(1, 4); @@ -175,7 +175,7 @@ * Work the jobs. */ - Artisan::call('queue:work', ['--stop-when-empty' => true]); + Artisan::call('queue:work', ['--stop-when-empty' => true, '--sleep' => 0]); expect(Queue::size())->toBe(0); expect(Pulse::ignore(fn () => DB::table('pulse_entries')->where('type', 'slow_job')->count()))->toBe(0); @@ -210,7 +210,7 @@ * Work the jobs. */ - Artisan::call('queue:work', ['--stop-when-empty' => true]); + Artisan::call('queue:work', ['--stop-when-empty' => true, '--sleep' => 0]); expect(Queue::size())->toBe(0); expect(Pulse::ignore(fn () => DB::table('pulse_entries')->where('type', 'slow_job')->count()))->toBe(10); From a76c13c96847a55e122a7606e3f6a80c2a4ddeee Mon Sep 17 00:00:00 2001 From: Jess Archer Date: Fri, 8 Dec 2023 14:54:10 +1000 Subject: [PATCH 050/110] Fix flaky test --- tests/Feature/Recorders/SlowQueriesTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Feature/Recorders/SlowQueriesTest.php b/tests/Feature/Recorders/SlowQueriesTest.php index cf5914b0..d4d2a7bf 100644 --- a/tests/Feature/Recorders/SlowQueriesTest.php +++ b/tests/Feature/Recorders/SlowQueriesTest.php @@ -28,7 +28,7 @@ $key = json_decode($entries[0]->key); expect($key[0])->toBe('select * from users'); expect($key[1])->not->toBeNull(); - $aggregates = Pulse::ignore(fn () => DB::table('pulse_aggregates')->orderBy('period')->get()); + $aggregates = Pulse::ignore(fn () => DB::table('pulse_aggregates')->orderBy('period')->orderBy('aggregate')->get()); expect($aggregates)->toHaveCount(8); expect($aggregates[0])->toHaveProperties([ 'bucket' => (int) (floor((now()->timestamp - 5) / 60) * 60), @@ -72,7 +72,7 @@ $key = json_decode($entries[0]->key); expect($key[0])->toBe('select * from users'); expect($key[1])->toBeNull(); - $aggregates = Pulse::ignore(fn () => DB::table('pulse_aggregates')->orderBy('period')->get()); + $aggregates = Pulse::ignore(fn () => DB::table('pulse_aggregates')->orderBy('period')->orderBy('aggregate')->get()); expect($aggregates)->toHaveCount(8); expect($aggregates[0])->toHaveProperties([ 'bucket' => (int) (floor((now()->timestamp - 5) / 60) * 60), From 66d3915370f0679ea12b1029a4d372bc1350d3d2 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Fri, 8 Dec 2023 12:37:28 +0800 Subject: [PATCH 051/110] Testbench Improvements Signed-off-by: Mior Muhammad Zaki --- composer.json | 2 +- testbench.yaml | 3 +- tests/Feature/Livewire/UsageTest.php | 16 +++---- tests/Pest.php | 5 +++ tests/TestCase.php | 16 +++---- .../0000_00_00_000001_create_pulse_tables.php | 44 ------------------- 6 files changed, 19 insertions(+), 67 deletions(-) delete mode 100644 tests/migrations/0000_00_00_000001_create_pulse_tables.php diff --git a/composer.json b/composer.json index 33be9f82..98f74bfd 100644 --- a/composer.json +++ b/composer.json @@ -37,7 +37,7 @@ "require-dev": { "guzzlehttp/guzzle": "^7.7", "mockery/mockery": "^1.0", - "orchestra/testbench": "^8.0", + "orchestra/testbench": "^8.16", "pestphp/pest": "^2.0", "pestphp/pest-plugin-laravel": "^2.2", "phpstan/phpstan": "^1.11", diff --git a/testbench.yaml b/testbench.yaml index 7eb80224..87f67954 100644 --- a/testbench.yaml +++ b/testbench.yaml @@ -1,7 +1,8 @@ providers: - Laravel\Pulse\PulseServiceProvider -migrations: true +migrations: + - database/migrations workbench: start: '/' diff --git a/tests/Feature/Livewire/UsageTest.php b/tests/Feature/Livewire/UsageTest.php index 4740e4ff..d6ac743e 100644 --- a/tests/Feature/Livewire/UsageTest.php +++ b/tests/Feature/Livewire/UsageTest.php @@ -7,6 +7,7 @@ use Laravel\Pulse\Facades\Pulse; use Laravel\Pulse\Livewire\Usage; use Livewire\Livewire; +use Orchestra\Testbench\Factories\UserFactory; it('includes the card on the dashboard', function () { $this @@ -47,9 +48,9 @@ Livewire::withQueryParams(['usage' => $query]) ->test(Usage::class, ['lazy' => false]) ->assertViewHas('userRequestCounts', collect([ - (object) ['count' => 6, 'user' => (object) ['id' => $users[0]->id, 'name' => $users[0]->name, 'extra' => '', 'avatar' => null]], - (object) ['count' => 4, 'user' => (object) ['id' => $users[1]->id, 'name' => $users[1]->name, 'extra' => '', 'avatar' => null]], - (object) ['count' => 2, 'user' => (object) ['id' => $users[2]->id, 'name' => $users[2]->name, 'extra' => '', 'avatar' => null]], + (object) ['count' => 6, 'user' => (object) ['id' => (string) $users[0]->id, 'name' => $users[0]->name, 'extra' => $users[0]->email, 'avatar' => avatar($users[0]->email)]], + (object) ['count' => 4, 'user' => (object) ['id' => (string) $users[1]->id, 'name' => $users[1]->name, 'extra' => $users[1]->email, 'avatar' => avatar($users[1]->email)]], + (object) ['count' => 2, 'user' => (object) ['id' => (string) $users[2]->id, 'name' => $users[2]->name, 'extra' => $users[2]->email, 'avatar' => avatar($users[2]->email)]], ])); })->with([ ['requests', 'user_request'], @@ -63,16 +64,9 @@ class User extends AuthUser protected static function newFactory() { - return new class extends Factory + return new class extends UserFactory { protected $model = User::class; - - public function definition() - { - return [ - 'name' => $this->faker->name(), - ]; - } }; } } diff --git a/tests/Pest.php b/tests/Pest.php index 98421b73..21c03885 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -161,3 +161,8 @@ function captureRedisCommands(callable $callback) $process->running() && $process->signal(SIGINT); } } + +function avatar(string $email) +{ + return sprintf('https://gravatar.com/avatar/%s?d=mp', hash('sha256', trim(strtolower($email)))); +}; diff --git a/tests/TestCase.php b/tests/TestCase.php index 4dfb0310..fc9ebb57 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -4,25 +4,21 @@ use Illuminate\Contracts\Config\Repository; use Illuminate\Foundation\Testing\RefreshDatabase; +use Orchestra\Testbench\Attributes\WithMigration; +use Orchestra\Testbench\Concerns\WithWorkbench; use Orchestra\Testbench\TestCase as OrchestraTestCase; abstract class TestCase extends OrchestraTestCase { - use RefreshDatabase; + use RefreshDatabase, WithWorkbench; protected $enablesPackageDiscoveries = true; - protected function getPackageProviders($app): array + protected function setUp(): void { - return [ - \Laravel\Pulse\PulseServiceProvider::class, - ]; - } + $this->usesTestingFeature(new WithMigration('laravel', 'queue')); - protected function defineDatabaseMigrations(): void - { - $this->loadMigrationsFrom(__DIR__.'/../database/migrations'); - $this->loadMigrationsFrom(__DIR__.'/migrations'); + parent::setUp(); } protected function defineEnvironment($app): void diff --git a/tests/migrations/0000_00_00_000001_create_pulse_tables.php b/tests/migrations/0000_00_00_000001_create_pulse_tables.php deleted file mode 100644 index d202c2f5..00000000 --- a/tests/migrations/0000_00_00_000001_create_pulse_tables.php +++ /dev/null @@ -1,44 +0,0 @@ -id(); - $table->string('name'); - $table->timestamps(); - }); - - Schema::create('jobs', function (Blueprint $table) { - $table->bigIncrements('id'); - $table->string('queue')->index(); - $table->longText('payload'); - $table->unsignedTinyInteger('attempts'); - $table->unsignedInteger('reserved_at')->nullable(); - $table->unsignedInteger('available_at'); - $table->unsignedInteger('created_at'); - }); - - Schema::create('failed_jobs', function (Blueprint $table) { - $table->id(); - $table->string('uuid')->unique(); - $table->text('connection'); - $table->text('queue'); - $table->longText('payload'); - $table->longText('exception'); - $table->timestamp('failed_at')->useCurrent(); - }); - } - - public function down() - { - Schema::dropIfExists('users'); - Schema::dropIfExists('jobs'); - Schema::dropIfExists('failed_jobs'); - } -}; From 9c65d50d22529b6034b520257d105ee9b279ca3d Mon Sep 17 00:00:00 2001 From: crynobone Date: Fri, 8 Dec 2023 04:38:25 +0000 Subject: [PATCH 052/110] Fix code styling --- tests/Feature/Livewire/UsageTest.php | 1 - tests/Pest.php | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/Feature/Livewire/UsageTest.php b/tests/Feature/Livewire/UsageTest.php index d6ac743e..45799190 100644 --- a/tests/Feature/Livewire/UsageTest.php +++ b/tests/Feature/Livewire/UsageTest.php @@ -1,6 +1,5 @@ Date: Sat, 9 Dec 2023 16:00:01 +0100 Subject: [PATCH 053/110] $timestamp ??= CarbonImmutable::now(); (#196) --- src/Pulse.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Pulse.php b/src/Pulse.php index 4c53f358..25c8d686 100644 --- a/src/Pulse.php +++ b/src/Pulse.php @@ -157,9 +157,7 @@ public function record( ?int $value = null, DateTimeInterface|int|null $timestamp = null, ): Entry { - if ($timestamp === null) { - $timestamp = CarbonImmutable::now(); - } + $timestamp ??= CarbonImmutable::now(); $entry = new Entry( timestamp: $timestamp instanceof DateTimeInterface ? $timestamp->getTimestamp() : $timestamp, @@ -184,9 +182,7 @@ public function set( string $value, DateTimeInterface|int|null $timestamp = null, ): Value { - if ($timestamp === null) { - $timestamp = CarbonImmutable::now(); - } + $timestamp ??= CarbonImmutable::now(); $value = new Value( timestamp: $timestamp instanceof DateTimeInterface ? $timestamp->getTimestamp() : $timestamp, From 30a9122f4cd5bffea3332b72b689bbbe1ad869c4 Mon Sep 17 00:00:00 2001 From: Dries Vints Date: Sat, 9 Dec 2023 16:16:40 +0100 Subject: [PATCH 054/110] Update composer.json description (#191) We usually keep these the same as the GitHub repo description. --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 98f74bfd..9f0301c9 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "laravel/pulse", - "description": "", + "description": "Laravel Pulse is a real-time application performance monitoring tool and dashboard for your Laravel application.", "keywords": ["laravel"], "homepage": "https://github.com/laravel/pulse", "license": "MIT", From 5e4e8e62858a45f8e362818fbf4daf96a9d73a40 Mon Sep 17 00:00:00 2001 From: Jess Archer Date: Sun, 10 Dec 2023 01:17:34 +1000 Subject: [PATCH 055/110] [1.x] Add "sum" and "min" aggregations (#188) * Remove unnecessary looping * Add sum aggregation * Add min aggregate * Update contract * Refactor --- src/Contracts/Storage.php | 6 +- src/Entry.php | 48 +++- src/Storage/DatabaseStorage.php | 237 ++++++++++++------ tests/Feature/Storage/DatabaseStorageTest.php | 203 +++++++++++++-- 4 files changed, 391 insertions(+), 103 deletions(-) diff --git a/src/Contracts/Storage.php b/src/Contracts/Storage.php index 504a9286..7e8cbf09 100644 --- a/src/Contracts/Storage.php +++ b/src/Contracts/Storage.php @@ -49,6 +49,7 @@ public function values(string $type, ?array $keys = null): Collection; * Retrieve aggregate values for plotting on a graph. * * @param list $types + * @param 'count'|'min'|'max'|'sum'|'avg' $aggregate * @return \Illuminate\Support\Collection>> */ public function graph(array $types, string $aggregate, CarbonInterval $interval): Collection; @@ -56,9 +57,10 @@ public function graph(array $types, string $aggregate, CarbonInterval $interval) /** * Retrieve aggregate values for the given type. * - * @param list $aggregates + * @param 'count'|'min'|'max'|'sum'|'avg'|list<'count'|'min'|'max'|'sum'|'avg'> $aggregates * @return \Illuminate\Support\Collection $types + * @param 'count'|'min'|'max'|'sum'|'avg' $aggregate * @return \Illuminate\Support\Collection */ public function aggregateTypes( @@ -93,6 +96,7 @@ public function aggregateTypes( * Retrieve an aggregate total for the given types. * * @param string|list $types + * @param 'count'|'min'|'max'|'sum'|'avg' $aggregate * @return \Illuminate\Support\Collection */ public function aggregateTotal( diff --git a/src/Entry.php b/src/Entry.php index 5b3e6647..54bc5ca8 100644 --- a/src/Entry.php +++ b/src/Entry.php @@ -7,7 +7,7 @@ class Entry /** * The aggregations to perform on the entry. * - * @var list<'count'|'max'|'avg'> + * @var list<'count'|'min'|'max'|'sum'|'avg'> */ protected array $aggregations = []; @@ -38,6 +38,16 @@ public function count(): static return $this; } + /** + * Capture the minimum aggregate. + */ + public function min(): static + { + $this->aggregations[] = 'min'; + + return $this; + } + /** * Capture the maximum aggregate. */ @@ -48,6 +58,16 @@ public function max(): static return $this; } + /** + * Capture the sum aggregate. + */ + public function sum(): static + { + $this->aggregations[] = 'sum'; + + return $this; + } + /** * Capture the average aggregate. */ @@ -68,6 +88,16 @@ public function onlyBuckets(): static return $this; } + /** + * Return the aggregations for the entry. + * + * @return list<'count'|'min'|'max'|'sum'|'avg'> + */ + public function aggregations(): array + { + return $this->aggregations; + } + /** * Determine whether the entry is marked for count aggregation. */ @@ -76,6 +106,14 @@ public function isCount(): bool return in_array('count', $this->aggregations); } + /** + * Determine whether the entry is marked for minimum aggregation. + */ + public function isMin(): bool + { + return in_array('min', $this->aggregations); + } + /** * Determine whether the entry is marked for maximum aggregation. */ @@ -84,6 +122,14 @@ public function isMax(): bool return in_array('max', $this->aggregations); } + /** + * Determine whether the entry is marked for sum aggregation. + */ + public function isSum(): bool + { + return in_array('sum', $this->aggregations); + } + /** * Determine whether the entry is marked for average aggregation. */ diff --git a/src/Storage/DatabaseStorage.php b/src/Storage/DatabaseStorage.php index 2c230243..0ae497aa 100644 --- a/src/Storage/DatabaseStorage.php +++ b/src/Storage/DatabaseStorage.php @@ -4,6 +4,7 @@ use Carbon\CarbonImmutable; use Carbon\CarbonInterval; +use Closure; use Illuminate\Config\Repository; use Illuminate\Database\Connection; use Illuminate\Database\DatabaseManager; @@ -55,18 +56,38 @@ public function store(Collection $items): void ->insert($chunk->map->attributes()->all()) ); + [$counts, $minimums, $maximums, $sums, $averages] = array_values($entries + ->reduce(function ($carry, $entry) { + foreach ($entry->aggregations() as $aggregation) { + $carry[$aggregation][] = $entry; + } + + return $carry; + }, ['count' => [], 'min' => [], 'max' => [], 'sum' => [], 'avg' => []]) + ); + $this - ->aggregateCounts($entries->filter->isCount()) + ->preaggregateCounts(collect($counts)) // @phpstan-ignore argument.templateType argument.templateType ->chunk($this->config->get('pulse.storage.database.chunk')) ->each(fn ($chunk) => $this->upsertCount($chunk->all())); $this - ->aggregateMaximums($entries->filter->isMax()) + ->preaggregateMinimums(collect($minimums)) // @phpstan-ignore argument.templateType argument.templateType + ->chunk($this->config->get('pulse.storage.database.chunk')) + ->each(fn ($chunk) => $this->upsertMin($chunk->all())); + + $this + ->preaggregateMaximums(collect($maximums)) // @phpstan-ignore argument.templateType argument.templateType ->chunk($this->config->get('pulse.storage.database.chunk')) ->each(fn ($chunk) => $this->upsertMax($chunk->all())); $this - ->aggregateAverages($entries->filter->isAvg()) + ->preaggregateSums(collect($sums)) // @phpstan-ignore argument.templateType argument.templateType + ->chunk($this->config->get('pulse.storage.database.chunk')) + ->each(fn ($chunk) => $this->upsertSum($chunk->all())); + + $this + ->preaggregateAverages(collect($averages)) // @phpstan-ignore argument.templateType argument.templateType ->chunk($this->config->get('pulse.storage.database.chunk')) ->each(fn ($chunk) => $this->upsertAvg($chunk->all())); @@ -155,6 +176,26 @@ protected function upsertCount(array $values): int ); } + /** + * Insert new records or update the existing ones and the minimum. + * + * @param list $values + */ + protected function upsertMin(array $values): int + { + return $this->connection()->table('pulse_aggregates')->upsert( + $values, + ['bucket', 'period', 'type', 'aggregate', 'key_hash'], + [ + 'value' => match ($driver = $this->connection()->getDriverName()) { + 'mysql' => new Expression('least(`value`, values(`value`))'), + 'pgsql' => new Expression('least("pulse_aggregates"."value", "excluded"."value")'), + default => throw new RuntimeException("Unsupported database driver [{$driver}]"), + }, + ] + ); + } + /** * Insert new records or update the existing ones and the maximum. * @@ -175,6 +216,26 @@ protected function upsertMax(array $values): int ); } + /** + * Insert new records or update the existing ones and the sum. + * + * @param list $values + */ + protected function upsertSum(array $values): int + { + return $this->connection()->table('pulse_aggregates')->upsert( + $values, + ['bucket', 'period', 'type', 'aggregate', 'key_hash'], + [ + 'value' => match ($driver = $this->connection()->getDriverName()) { + 'mysql' => new Expression('`value` + values(`value`)'), + 'pgsql' => new Expression('"pulse_aggregates"."value" + "excluded"."value"'), + default => throw new RuntimeException("Unsupported database driver [{$driver}]"), + }, + ] + ); + } + /** * Insert new records or update the existing ones and the average. * @@ -200,90 +261,89 @@ protected function upsertAvg(array $values): int } /** - * Get the count aggregates + * Pre-aggregate entry counts. * * @param \Illuminate\Support\Collection $entries * @return \Illuminate\Support\Collection */ - protected function aggregateCounts(Collection $entries): Collection + protected function preaggregateCounts(Collection $entries): Collection { - $aggregates = []; - - foreach ($entries as $entry) { - foreach ($this->periods() as $period) { - // Exclude entries that would be trimmed. - if ($entry->timestamp < CarbonImmutable::now()->subMinutes($period)->getTimestamp()) { - continue; - } - - $bucket = (int) (floor($entry->timestamp / $period) * $period); - - $key = $entry->type.':'.$period.':'.$bucket.':'.$entry->key; - - if (! isset($aggregates[$key])) { - $aggregates[$key] = [ - 'bucket' => $bucket, - 'period' => $period, - 'type' => $entry->type, - 'aggregate' => 'count', - 'key' => $entry->key, - 'value' => 1, - ]; - } else { - $aggregates[$key]['value']++; - } - } - } - - return collect(array_values($aggregates)); + return $this->preaggregate($entries, 'count', fn ($aggregate) => [ + ...$aggregate, + 'value' => ($aggregate['value'] ?? 0) + 1, + ]); } /** - * Get the maximum aggregates + * Pre-aggregate entry minimums. * * @param \Illuminate\Support\Collection $entries * @return \Illuminate\Support\Collection */ - protected function aggregateMaximums(Collection $entries): Collection + protected function preaggregateMinimums(Collection $entries): Collection { - $aggregates = []; - - foreach ($entries as $entry) { - foreach ($this->periods() as $period) { - // Exclude entries that would be trimmed. - if ($entry->timestamp < CarbonImmutable::now()->subMinutes($period)->getTimestamp()) { - continue; - } - - $bucket = (int) (floor($entry->timestamp / $period) * $period); + return $this->preaggregate($entries, 'min', fn ($aggregate, $entry) => [ + ...$aggregate, + 'value' => ! isset($aggregate['value']) + ? $entry->value + : (int) min($aggregate['value'], $entry->value), + ]); + } - $key = $entry->type.':'.$period.':'.$bucket.':'.$entry->key; + /** + * Pre-aggregate entry maximums. + * + * @param \Illuminate\Support\Collection $entries + * @return \Illuminate\Support\Collection + */ + protected function preaggregateMaximums(Collection $entries): Collection + { + return $this->preaggregate($entries, 'max', fn ($aggregate, $entry) => [ + ...$aggregate, + 'value' => ! isset($aggregate['value']) + ? $entry->value + : (int) max($aggregate['value'], $entry->value), + ]); + } - if (! isset($aggregates[$key])) { - $aggregates[$key] = [ - 'bucket' => $bucket, - 'period' => $period, - 'type' => $entry->type, - 'aggregate' => 'max', - 'key' => $entry->key, - 'value' => (int) $entry->value, - ]; - } else { - $aggregates[$key]['value'] = (int) max($aggregates[$key]['value'], $entry->value); - } - } - } + /** + * Pre-aggregate entry sums. + * + * @param \Illuminate\Support\Collection $entries + * @return \Illuminate\Support\Collection + */ + protected function preaggregateSums(Collection $entries): Collection + { + return $this->preaggregate($entries, 'sum', fn ($aggregate, $entry) => [ + ...$aggregate, + 'value' => ($aggregate['value'] ?? 0) + $entry->value, + ]); + } - return collect(array_values($aggregates)); + /** + * Pre-aggregate entry averages. + * + * @param \Illuminate\Support\Collection $entries + * @return \Illuminate\Support\Collection + */ + protected function preaggregateAverages(Collection $entries): Collection + { + return $this->preaggregate($entries, 'avg', fn ($aggregate, $entry) => [ + ...$aggregate, + 'value' => ! isset($aggregate['value']) + ? $entry->value + : ($aggregate['value'] * $aggregate['count'] + $entry->value) / ($aggregate['count'] + 1), + 'count' => ($aggregate['count'] ?? 0) + 1, + ]); } /** - * Get the average aggregates + * Pre-aggregate entries with a callback. * * @param \Illuminate\Support\Collection $entries * @return \Illuminate\Support\Collection */ - protected function aggregateAverages(Collection $entries): Collection + protected function preaggregate(Collection $entries, string $aggregate, Closure $callback): Collection { $aggregates = []; @@ -299,18 +359,15 @@ protected function aggregateAverages(Collection $entries): Collection $key = $entry->type.':'.$period.':'.$bucket.':'.$entry->key; if (! isset($aggregates[$key])) { - $aggregates[$key] = [ + $aggregates[$key] = $callback([ 'bucket' => $bucket, 'period' => $period, 'type' => $entry->type, - 'aggregate' => 'avg', + 'aggregate' => $aggregate, 'key' => $entry->key, - 'value' => (int) $entry->value, - 'count' => 1, - ]; + ], $entry); } else { - $aggregates[$key]['value'] = ($aggregates[$key]['value'] * $aggregates[$key]['count'] + $entry->value) / ($aggregates[$key]['count'] + 1); - $aggregates[$key]['count']++; + $aggregates[$key] = $callback($aggregates[$key], $entry); } } } @@ -364,11 +421,15 @@ public function values(string $type, ?array $keys = null): Collection * Retrieve aggregate values for plotting on a graph. * * @param list $types - * @param 'count'|'max'|'avg' $aggregate + * @param 'count'|'min'|'max'|'sum'|'avg' $aggregate * @return \Illuminate\Support\Collection>> */ public function graph(array $types, string $aggregate, CarbonInterval $interval): Collection { + if (! in_array($aggregate, $allowed = ['count', 'min', 'max', 'sum', 'avg'])) { + throw new InvalidArgumentException("Invalid aggregate type [$aggregate], allowed types: [".implode(', ', $allowed).'].'); + } + $now = CarbonImmutable::now(); $period = $interval->totalSeconds / 60; $maxDataPoints = 60; @@ -405,10 +466,12 @@ public function graph(array $types, string $aggregate, CarbonInterval $interval) /** * Retrieve aggregate values for the given type. * - * @param 'count'|'max'|'avg'|list<'count'|'max'|'avg'> $aggregates + * @param 'count'|'min'|'max'|'sum'|'avg'|list<'count'|'min'|'max'|'sum'|'avg'> $aggregates * @return \Illuminate\Support\Collection @@ -423,7 +486,7 @@ public function aggregate( ): Collection { $aggregates = is_array($aggregates) ? $aggregates : [$aggregates]; - if ($invalid = array_diff($aggregates, $allowed = ['count', 'max', 'avg'])) { + if ($invalid = array_diff($aggregates, $allowed = ['count', 'min', 'max', 'sum', 'avg'])) { throw new InvalidArgumentException('Invalid aggregate type(s) ['.implode(', ', $invalid).'], allowed types: ['.implode(', ', $allowed).'].'); } @@ -445,7 +508,9 @@ public function aggregate( foreach ($aggregates as $aggregate) { $query->selectRaw(match ($aggregate) { 'count' => "sum({$this->wrap('count')})", + 'min' => "min({$this->wrap('min')})", 'max' => "max({$this->wrap('max')})", + 'sum' => "sum({$this->wrap('sum')})", 'avg' => "avg({$this->wrap('avg')})", }." as {$this->wrap($aggregate)}"); } @@ -463,7 +528,9 @@ public function aggregate( foreach ($aggregates as $aggregate) { $query->selectRaw(match ($aggregate) { 'count' => 'count(*)', + 'min' => "min({$this->wrap('value')})", 'max' => "max({$this->wrap('value')})", + 'sum' => "sum({$this->wrap('value')})", 'avg' => "avg({$this->wrap('value')})", }." as {$this->wrap($aggregate)}"); } @@ -484,7 +551,9 @@ public function aggregate( if ($aggregate === $currentAggregate) { $query->selectRaw(match ($aggregate) { 'count' => "sum({$this->wrap('value')})", + 'min' => "min({$this->wrap('value')})", 'max' => "max({$this->wrap('value')})", + 'sum' => "sum({$this->wrap('value')})", 'avg' => "avg({$this->wrap('value')})", }." as {$this->wrap($aggregate)}"); } else { @@ -513,7 +582,7 @@ public function aggregate( * Retrieve aggregate values for the given types. * * @param string|list $types - * @param 'count'|'max'|'avg' $aggregate + * @param 'count'|'min'|'max'|'sum'|'avg' $aggregate * @return \Illuminate\Support\Collection */ public function aggregateTypes( @@ -524,7 +593,7 @@ public function aggregateTypes( string $direction = 'desc', int $limit = 101, ): Collection { - if (! in_array($aggregate, $allowed = ['count', 'max', 'avg'])) { + if (! in_array($aggregate, $allowed = ['count', 'min', 'max', 'sum', 'avg'])) { throw new InvalidArgumentException("Invalid aggregate type [$aggregate], allowed types: [".implode(', ', $allowed).'].'); } @@ -547,7 +616,9 @@ public function aggregateTypes( foreach ($types as $type) { $query->selectRaw(match ($aggregate) { 'count' => "sum({$this->wrap($type)})", + 'min' => "min({$this->wrap($type)})", 'max' => "max({$this->wrap($type)})", + 'sum' => "sum({$this->wrap($type)})", 'avg' => "avg({$this->wrap($type)})", }." as {$this->wrap($type)}"); } @@ -565,7 +636,9 @@ public function aggregateTypes( foreach ($types as $type) { $query->selectRaw(match ($aggregate) { 'count' => "count(case when ({$this->wrap('type')} = ?) then true else null end)", + 'min' => "min(case when ({$this->wrap('type')} = ?) then {$this->wrap('value')} else null end)", 'max' => "max(case when ({$this->wrap('type')} = ?) then {$this->wrap('value')} else null end)", + 'sum' => "sum(case when ({$this->wrap('type')} = ?) then {$this->wrap('value')} else null end)", 'avg' => "avg(case when ({$this->wrap('type')} = ?) then {$this->wrap('value')} else null end)", }." as {$this->wrap($type)}", [$type]); } @@ -584,7 +657,9 @@ public function aggregateTypes( foreach ($types as $type) { $query->selectRaw(match ($aggregate) { 'count' => "sum(case when ({$this->wrap('type')} = ?) then {$this->wrap('value')} else null end)", + 'min' => "min(case when ({$this->wrap('type')} = ?) then {$this->wrap('value')} else null end)", 'max' => "max(case when ({$this->wrap('type')} = ?) then {$this->wrap('value')} else null end)", + 'sum' => "sum(case when ({$this->wrap('type')} = ?) then {$this->wrap('value')} else null end)", 'avg' => "avg(case when ({$this->wrap('type')} = ?) then {$this->wrap('value')} else null end)", }." as {$this->wrap($type)}", [$type]); } @@ -609,7 +684,7 @@ public function aggregateTypes( * Retrieve an aggregate total for the given types. * * @param string|list $types - * @param 'count'|'max'|'avg' $aggregate + * @param 'count'|'min'|'max'|'sum'|'avg' $aggregate * @return \Illuminate\Support\Collection */ public function aggregateTotal( @@ -617,7 +692,7 @@ public function aggregateTotal( string $aggregate, CarbonInterval $interval, ): Collection { - if (! in_array($aggregate, $allowed = ['count', 'max', 'avg'])) { + if (! in_array($aggregate, $allowed = ['count', 'min', 'max', 'sum', 'avg'])) { throw new InvalidArgumentException("Invalid aggregate type [$aggregate], allowed types: [".implode(', ', $allowed).'].'); } @@ -635,7 +710,9 @@ public function aggregateTotal( ->addSelect('type') ->selectRaw(match ($aggregate) { 'count' => "sum({$this->wrap('count')})", + 'min' => "min({$this->wrap('min')})", 'max' => "max({$this->wrap('max')})", + 'sum' => "sum({$this->wrap('sum')})", 'avg' => "avg({$this->wrap('avg')})", }." as {$this->wrap($aggregate)}") ->fromSub(fn (Builder $query) => $query @@ -643,7 +720,9 @@ public function aggregateTotal( ->addSelect('type') ->selectRaw(match ($aggregate) { 'count' => 'count(*)', + 'min' => "min({$this->wrap('value')})", 'max' => "max({$this->wrap('value')})", + 'sum' => "sum({$this->wrap('value')})", 'avg' => "avg({$this->wrap('value')})", }." as {$this->wrap($aggregate)}") ->from('pulse_entries') @@ -656,7 +735,9 @@ public function aggregateTotal( ->select('type') ->selectRaw(match ($aggregate) { 'count' => "sum({$this->wrap('value')})", + 'min' => "min({$this->wrap('value')})", 'max' => "max({$this->wrap('value')})", + 'sum' => "sum({$this->wrap('value')})", 'avg' => "avg({$this->wrap('value')})", }." as {$this->wrap($aggregate)}") ->from('pulse_aggregates') diff --git a/tests/Feature/Storage/DatabaseStorageTest.php b/tests/Feature/Storage/DatabaseStorageTest.php index 93fc05c4..966de06e 100644 --- a/tests/Feature/Storage/DatabaseStorageTest.php +++ b/tests/Feature/Storage/DatabaseStorageTest.php @@ -5,6 +5,121 @@ use Illuminate\Support\Facades\DB; use Laravel\Pulse\Facades\Pulse; +test('aggregation', function () { + Pulse::record('type', 'key1', 200)->count()->min()->max()->sum()->avg(); + Pulse::record('type', 'key1', 100)->count()->min()->max()->sum()->avg(); + Pulse::record('type', 'key2', 400)->count()->min()->max()->sum()->avg(); + Pulse::store(); + + $entries = Pulse::ignore(fn () => DB::table('pulse_entries')->orderBy('id')->get()); + expect($entries)->toHaveCount(3); + expect($entries[0])->toHaveProperties(['type' => 'type', 'key' => 'key1', 'value' => 200]); + expect($entries[1])->toHaveProperties(['type' => 'type', 'key' => 'key1', 'value' => 100]); + expect($entries[2])->toHaveProperties(['type' => 'type', 'key' => 'key2', 'value' => 400]); + + $aggregates = Pulse::ignore(fn () => DB::table('pulse_aggregates')->orderBy('period')->orderBy('aggregate')->orderBy('key')->get()); + expect($aggregates)->toHaveCount(40); // 2 entries * 5 aggregates * 4 periods + expect($aggregates[0])->toHaveProperties(['type' => 'type', 'period' => 60, 'aggregate' => 'avg', 'key' => 'key1', 'value' => 150]); + expect($aggregates[1])->toHaveProperties(['type' => 'type', 'period' => 60, 'aggregate' => 'avg', 'key' => 'key2', 'value' => 400]); + expect($aggregates[2])->toHaveProperties(['type' => 'type', 'period' => 60, 'aggregate' => 'count', 'key' => 'key1', 'value' => 2]); + expect($aggregates[3])->toHaveProperties(['type' => 'type', 'period' => 60, 'aggregate' => 'count', 'key' => 'key2', 'value' => 1]); + expect($aggregates[4])->toHaveProperties(['type' => 'type', 'period' => 60, 'aggregate' => 'max', 'key' => 'key1', 'value' => 200]); + expect($aggregates[5])->toHaveProperties(['type' => 'type', 'period' => 60, 'aggregate' => 'max', 'key' => 'key2', 'value' => 400]); + expect($aggregates[6])->toHaveProperties(['type' => 'type', 'period' => 60, 'aggregate' => 'min', 'key' => 'key1', 'value' => 100]); + expect($aggregates[7])->toHaveProperties(['type' => 'type', 'period' => 60, 'aggregate' => 'min', 'key' => 'key2', 'value' => 400]); + expect($aggregates[8])->toHaveProperties(['type' => 'type', 'period' => 60, 'aggregate' => 'sum', 'key' => 'key1', 'value' => 300]); + expect($aggregates[9])->toHaveProperties(['type' => 'type', 'period' => 60, 'aggregate' => 'sum', 'key' => 'key2', 'value' => 400]); + + expect($aggregates[10])->toHaveProperties(['type' => 'type', 'period' => 360, 'aggregate' => 'avg', 'key' => 'key1', 'value' => 150]); + expect($aggregates[11])->toHaveProperties(['type' => 'type', 'period' => 360, 'aggregate' => 'avg', 'key' => 'key2', 'value' => 400]); + expect($aggregates[12])->toHaveProperties(['type' => 'type', 'period' => 360, 'aggregate' => 'count', 'key' => 'key1', 'value' => 2]); + expect($aggregates[13])->toHaveProperties(['type' => 'type', 'period' => 360, 'aggregate' => 'count', 'key' => 'key2', 'value' => 1]); + expect($aggregates[14])->toHaveProperties(['type' => 'type', 'period' => 360, 'aggregate' => 'max', 'key' => 'key1', 'value' => 200]); + expect($aggregates[15])->toHaveProperties(['type' => 'type', 'period' => 360, 'aggregate' => 'max', 'key' => 'key2', 'value' => 400]); + expect($aggregates[16])->toHaveProperties(['type' => 'type', 'period' => 360, 'aggregate' => 'min', 'key' => 'key1', 'value' => 100]); + expect($aggregates[17])->toHaveProperties(['type' => 'type', 'period' => 360, 'aggregate' => 'min', 'key' => 'key2', 'value' => 400]); + expect($aggregates[18])->toHaveProperties(['type' => 'type', 'period' => 360, 'aggregate' => 'sum', 'key' => 'key1', 'value' => 300]); + expect($aggregates[19])->toHaveProperties(['type' => 'type', 'period' => 360, 'aggregate' => 'sum', 'key' => 'key2', 'value' => 400]); + + expect($aggregates[20])->toHaveProperties(['type' => 'type', 'period' => 1440, 'aggregate' => 'avg', 'key' => 'key1', 'value' => 150]); + expect($aggregates[21])->toHaveProperties(['type' => 'type', 'period' => 1440, 'aggregate' => 'avg', 'key' => 'key2', 'value' => 400]); + expect($aggregates[22])->toHaveProperties(['type' => 'type', 'period' => 1440, 'aggregate' => 'count', 'key' => 'key1', 'value' => 2]); + expect($aggregates[23])->toHaveProperties(['type' => 'type', 'period' => 1440, 'aggregate' => 'count', 'key' => 'key2', 'value' => 1]); + expect($aggregates[24])->toHaveProperties(['type' => 'type', 'period' => 1440, 'aggregate' => 'max', 'key' => 'key1', 'value' => 200]); + expect($aggregates[25])->toHaveProperties(['type' => 'type', 'period' => 1440, 'aggregate' => 'max', 'key' => 'key2', 'value' => 400]); + expect($aggregates[26])->toHaveProperties(['type' => 'type', 'period' => 1440, 'aggregate' => 'min', 'key' => 'key1', 'value' => 100]); + expect($aggregates[27])->toHaveProperties(['type' => 'type', 'period' => 1440, 'aggregate' => 'min', 'key' => 'key2', 'value' => 400]); + expect($aggregates[28])->toHaveProperties(['type' => 'type', 'period' => 1440, 'aggregate' => 'sum', 'key' => 'key1', 'value' => 300]); + expect($aggregates[29])->toHaveProperties(['type' => 'type', 'period' => 1440, 'aggregate' => 'sum', 'key' => 'key2', 'value' => 400]); + + expect($aggregates[30])->toHaveProperties(['type' => 'type', 'period' => 10080, 'aggregate' => 'avg', 'key' => 'key1', 'value' => 150]); + expect($aggregates[31])->toHaveProperties(['type' => 'type', 'period' => 10080, 'aggregate' => 'avg', 'key' => 'key2', 'value' => 400]); + expect($aggregates[32])->toHaveProperties(['type' => 'type', 'period' => 10080, 'aggregate' => 'count', 'key' => 'key1', 'value' => 2]); + expect($aggregates[33])->toHaveProperties(['type' => 'type', 'period' => 10080, 'aggregate' => 'count', 'key' => 'key2', 'value' => 1]); + expect($aggregates[34])->toHaveProperties(['type' => 'type', 'period' => 10080, 'aggregate' => 'max', 'key' => 'key1', 'value' => 200]); + expect($aggregates[35])->toHaveProperties(['type' => 'type', 'period' => 10080, 'aggregate' => 'max', 'key' => 'key2', 'value' => 400]); + expect($aggregates[36])->toHaveProperties(['type' => 'type', 'period' => 10080, 'aggregate' => 'min', 'key' => 'key1', 'value' => 100]); + expect($aggregates[37])->toHaveProperties(['type' => 'type', 'period' => 10080, 'aggregate' => 'min', 'key' => 'key2', 'value' => 400]); + expect($aggregates[38])->toHaveProperties(['type' => 'type', 'period' => 10080, 'aggregate' => 'sum', 'key' => 'key1', 'value' => 300]); + expect($aggregates[39])->toHaveProperties(['type' => 'type', 'period' => 10080, 'aggregate' => 'sum', 'key' => 'key2', 'value' => 400]); + + Pulse::record('type', 'key1', 600)->count()->min()->max()->sum()->avg(); + Pulse::store(); + + $entries = Pulse::ignore(fn () => DB::table('pulse_entries')->orderBy('id')->get()); + expect($entries)->toHaveCount(4); + expect($entries[0])->toHaveProperties(['type' => 'type', 'key' => 'key1', 'value' => 200]); + expect($entries[1])->toHaveProperties(['type' => 'type', 'key' => 'key1', 'value' => 100]); + expect($entries[2])->toHaveProperties(['type' => 'type', 'key' => 'key2', 'value' => 400]); + expect($entries[3])->toHaveProperties(['type' => 'type', 'key' => 'key1', 'value' => 600]); + + $aggregates = Pulse::ignore(fn () => DB::table('pulse_aggregates')->orderBy('period')->orderBy('aggregate')->orderBy('key')->get()); + expect($aggregates)->toHaveCount(40); // 2 entries * 5 aggregates * 4 periods + expect($aggregates[0])->toHaveProperties(['type' => 'type', 'period' => 60, 'aggregate' => 'avg', 'key' => 'key1', 'value' => 300]); + expect($aggregates[1])->toHaveProperties(['type' => 'type', 'period' => 60, 'aggregate' => 'avg', 'key' => 'key2', 'value' => 400]); + expect($aggregates[2])->toHaveProperties(['type' => 'type', 'period' => 60, 'aggregate' => 'count', 'key' => 'key1', 'value' => 3]); + expect($aggregates[3])->toHaveProperties(['type' => 'type', 'period' => 60, 'aggregate' => 'count', 'key' => 'key2', 'value' => 1]); + expect($aggregates[4])->toHaveProperties(['type' => 'type', 'period' => 60, 'aggregate' => 'max', 'key' => 'key1', 'value' => 600]); + expect($aggregates[5])->toHaveProperties(['type' => 'type', 'period' => 60, 'aggregate' => 'max', 'key' => 'key2', 'value' => 400]); + expect($aggregates[6])->toHaveProperties(['type' => 'type', 'period' => 60, 'aggregate' => 'min', 'key' => 'key1', 'value' => 100]); + expect($aggregates[7])->toHaveProperties(['type' => 'type', 'period' => 60, 'aggregate' => 'min', 'key' => 'key2', 'value' => 400]); + expect($aggregates[8])->toHaveProperties(['type' => 'type', 'period' => 60, 'aggregate' => 'sum', 'key' => 'key1', 'value' => 900]); + expect($aggregates[9])->toHaveProperties(['type' => 'type', 'period' => 60, 'aggregate' => 'sum', 'key' => 'key2', 'value' => 400]); + + expect($aggregates[10])->toHaveProperties(['type' => 'type', 'period' => 360, 'aggregate' => 'avg', 'key' => 'key1', 'value' => 300]); + expect($aggregates[11])->toHaveProperties(['type' => 'type', 'period' => 360, 'aggregate' => 'avg', 'key' => 'key2', 'value' => 400]); + expect($aggregates[12])->toHaveProperties(['type' => 'type', 'period' => 360, 'aggregate' => 'count', 'key' => 'key1', 'value' => 3]); + expect($aggregates[13])->toHaveProperties(['type' => 'type', 'period' => 360, 'aggregate' => 'count', 'key' => 'key2', 'value' => 1]); + expect($aggregates[14])->toHaveProperties(['type' => 'type', 'period' => 360, 'aggregate' => 'max', 'key' => 'key1', 'value' => 600]); + expect($aggregates[15])->toHaveProperties(['type' => 'type', 'period' => 360, 'aggregate' => 'max', 'key' => 'key2', 'value' => 400]); + expect($aggregates[16])->toHaveProperties(['type' => 'type', 'period' => 360, 'aggregate' => 'min', 'key' => 'key1', 'value' => 100]); + expect($aggregates[17])->toHaveProperties(['type' => 'type', 'period' => 360, 'aggregate' => 'min', 'key' => 'key2', 'value' => 400]); + expect($aggregates[18])->toHaveProperties(['type' => 'type', 'period' => 360, 'aggregate' => 'sum', 'key' => 'key1', 'value' => 900]); + expect($aggregates[19])->toHaveProperties(['type' => 'type', 'period' => 360, 'aggregate' => 'sum', 'key' => 'key2', 'value' => 400]); + + expect($aggregates[20])->toHaveProperties(['type' => 'type', 'period' => 1440, 'aggregate' => 'avg', 'key' => 'key1', 'value' => 300]); + expect($aggregates[21])->toHaveProperties(['type' => 'type', 'period' => 1440, 'aggregate' => 'avg', 'key' => 'key2', 'value' => 400]); + expect($aggregates[22])->toHaveProperties(['type' => 'type', 'period' => 1440, 'aggregate' => 'count', 'key' => 'key1', 'value' => 3]); + expect($aggregates[23])->toHaveProperties(['type' => 'type', 'period' => 1440, 'aggregate' => 'count', 'key' => 'key2', 'value' => 1]); + expect($aggregates[24])->toHaveProperties(['type' => 'type', 'period' => 1440, 'aggregate' => 'max', 'key' => 'key1', 'value' => 600]); + expect($aggregates[25])->toHaveProperties(['type' => 'type', 'period' => 1440, 'aggregate' => 'max', 'key' => 'key2', 'value' => 400]); + expect($aggregates[26])->toHaveProperties(['type' => 'type', 'period' => 1440, 'aggregate' => 'min', 'key' => 'key1', 'value' => 100]); + expect($aggregates[27])->toHaveProperties(['type' => 'type', 'period' => 1440, 'aggregate' => 'min', 'key' => 'key2', 'value' => 400]); + expect($aggregates[28])->toHaveProperties(['type' => 'type', 'period' => 1440, 'aggregate' => 'sum', 'key' => 'key1', 'value' => 900]); + expect($aggregates[29])->toHaveProperties(['type' => 'type', 'period' => 1440, 'aggregate' => 'sum', 'key' => 'key2', 'value' => 400]); + + expect($aggregates[30])->toHaveProperties(['type' => 'type', 'period' => 10080, 'aggregate' => 'avg', 'key' => 'key1', 'value' => 300]); + expect($aggregates[31])->toHaveProperties(['type' => 'type', 'period' => 10080, 'aggregate' => 'avg', 'key' => 'key2', 'value' => 400]); + expect($aggregates[32])->toHaveProperties(['type' => 'type', 'period' => 10080, 'aggregate' => 'count', 'key' => 'key1', 'value' => 3]); + expect($aggregates[33])->toHaveProperties(['type' => 'type', 'period' => 10080, 'aggregate' => 'count', 'key' => 'key2', 'value' => 1]); + expect($aggregates[34])->toHaveProperties(['type' => 'type', 'period' => 10080, 'aggregate' => 'max', 'key' => 'key1', 'value' => 600]); + expect($aggregates[35])->toHaveProperties(['type' => 'type', 'period' => 10080, 'aggregate' => 'max', 'key' => 'key2', 'value' => 400]); + expect($aggregates[36])->toHaveProperties(['type' => 'type', 'period' => 10080, 'aggregate' => 'min', 'key' => 'key1', 'value' => 100]); + expect($aggregates[37])->toHaveProperties(['type' => 'type', 'period' => 10080, 'aggregate' => 'min', 'key' => 'key2', 'value' => 400]); + expect($aggregates[38])->toHaveProperties(['type' => 'type', 'period' => 10080, 'aggregate' => 'sum', 'key' => 'key1', 'value' => 900]); + expect($aggregates[39])->toHaveProperties(['type' => 'type', 'period' => 10080, 'aggregate' => 'sum', 'key' => 'key2', 'value' => 400]); +}); + it('combines duplicate count aggregates before upserting', function () { $queries = collect(); DB::listen(fn ($query) => $queries[] = $query); @@ -26,6 +141,27 @@ expect($aggregates['key2'])->toEqual(1); }); +it('combines duplicate min aggregates before upserting', function () { + $queries = collect(); + DB::listen(fn ($query) => $queries[] = $query); + + Pulse::record('type', 'key1', 200)->min(); + Pulse::record('type', 'key1', 100)->min(); + Pulse::record('type', 'key1', 300)->min(); + Pulse::record('type', 'key2', 100)->min(); + Pulse::store(); + + expect($queries)->toHaveCount(2); + expect($queries[0]->sql)->toContain('pulse_entries'); + expect($queries[1]->sql)->toContain('pulse_aggregates'); + expect($queries[0]->bindings)->toHaveCount(4 * 4); // 4 entries, 4 columns each + expect($queries[1]->bindings)->toHaveCount(2 * 6 * 4); // 2 entries, 6 columns each, 4 periods + + $aggregates = Pulse::ignore(fn () => DB::table('pulse_aggregates')->where('period', 60)->orderBy('key')->pluck('value', 'key')); + expect($aggregates['key1'])->toEqual(100); + expect($aggregates['key2'])->toEqual(100); +}); + it('combines duplicate max aggregates before upserting', function () { $queries = collect(); DB::listen(fn ($query) => $queries[] = $query); @@ -47,6 +183,27 @@ expect($aggregates['key2'])->toEqual(100); }); +it('combines duplicate sum aggregates before upserting', function () { + $queries = collect(); + DB::listen(fn ($query) => $queries[] = $query); + + Pulse::record('type', 'key1', 100)->sum(); + Pulse::record('type', 'key1', 300)->sum(); + Pulse::record('type', 'key1', 200)->sum(); + Pulse::record('type', 'key2', 100)->sum(); + Pulse::store(); + + expect($queries)->toHaveCount(2); + expect($queries[0]->sql)->toContain('pulse_entries'); + expect($queries[1]->sql)->toContain('pulse_aggregates'); + expect($queries[0]->bindings)->toHaveCount(4 * 4); // 4 entries, 4 columns each + expect($queries[1]->bindings)->toHaveCount(2 * 6 * 4); // 2 entries, 6 columns each, 4 periods + + $aggregates = Pulse::ignore(fn () => DB::table('pulse_aggregates')->where('period', 60)->orderBy('key')->pluck('value', 'key')); + expect($aggregates['key1'])->toEqual(600); + expect($aggregates['key2'])->toEqual(100); +}); + it('combines duplicate average aggregates before upserting', function () { $queries = collect(); DB::listen(fn ($query) => $queries[] = $query); @@ -80,36 +237,36 @@ test('one or more aggregates for a single type', function () { /* - | key | max | avg | count | - |----------|-----|-----|-------| - | GET /bar | 600 | 400 | 6 | - | GET /foo | 300 | 200 | 6 | + | key | min | max | sum | avg | count | + |----------|-----|-----|------|-----|-------| + | GET /bar | 200 | 600 | 2400 | 400 | 6 | + | GET /foo | 100 | 300 | 2000 | 200 | 6 | */ // Add entries outside of the window Carbon::setTestNow('2000-01-01 12:00:00'); - Pulse::record('slow_request', 'GET /foo', 100)->max()->avg()->count(); - Pulse::record('slow_request', 'GET /bar', 200)->max()->avg()->count(); + Pulse::record('slow_request', 'GET /foo', 100)->min()->max()->sum()->avg()->count(); + Pulse::record('slow_request', 'GET /bar', 200)->min()->max()->sum()->avg()->count(); // Add entries to the "tail" Carbon::setTestNow('2000-01-01 12:00:01'); - Pulse::record('slow_request', 'GET /foo', 100)->max()->avg()->count(); - Pulse::record('slow_request', 'GET /foo', 200)->max()->avg()->count(); - Pulse::record('slow_request', 'GET /foo', 300)->max()->avg()->count(); - Pulse::record('slow_request', 'GET /foo', 400)->max()->avg()->count(); - Pulse::record('slow_request', 'GET /bar', 200)->max()->avg()->count(); - Pulse::record('slow_request', 'GET /bar', 400)->max()->avg()->count(); - Pulse::record('slow_request', 'GET /bar', 600)->max()->avg()->count(); + Pulse::record('slow_request', 'GET /foo', 100)->min()->max()->sum()->avg()->count(); + Pulse::record('slow_request', 'GET /foo', 200)->min()->max()->sum()->avg()->count(); + Pulse::record('slow_request', 'GET /foo', 300)->min()->max()->sum()->avg()->count(); + Pulse::record('slow_request', 'GET /foo', 400)->min()->max()->sum()->avg()->count(); + Pulse::record('slow_request', 'GET /bar', 200)->min()->max()->sum()->avg()->count(); + Pulse::record('slow_request', 'GET /bar', 400)->min()->max()->sum()->avg()->count(); + Pulse::record('slow_request', 'GET /bar', 600)->min()->max()->sum()->avg()->count(); // Add entries to the current buckets. Carbon::setTestNow('2000-01-01 12:59:00'); - Pulse::record('slow_request', 'GET /foo', 100)->max()->avg()->count(); - Pulse::record('slow_request', 'GET /foo', 200)->max()->avg()->count(); - Pulse::record('slow_request', 'GET /foo', 300)->max()->avg()->count(); - Pulse::record('slow_request', 'GET /foo', 400)->max()->avg()->count(); - Pulse::record('slow_request', 'GET /bar', 200)->max()->avg()->count(); - Pulse::record('slow_request', 'GET /bar', 400)->max()->avg()->count(); - Pulse::record('slow_request', 'GET /bar', 600)->max()->avg()->count(); + Pulse::record('slow_request', 'GET /foo', 100)->min()->max()->sum()->avg()->count(); + Pulse::record('slow_request', 'GET /foo', 200)->min()->max()->sum()->avg()->count(); + Pulse::record('slow_request', 'GET /foo', 300)->min()->max()->sum()->avg()->count(); + Pulse::record('slow_request', 'GET /foo', 400)->min()->max()->sum()->avg()->count(); + Pulse::record('slow_request', 'GET /bar', 200)->min()->max()->sum()->avg()->count(); + Pulse::record('slow_request', 'GET /bar', 400)->min()->max()->sum()->avg()->count(); + Pulse::record('slow_request', 'GET /bar', 600)->min()->max()->sum()->avg()->count(); Pulse::store(); @@ -122,11 +279,11 @@ (object) ['key' => 'GET /bar', 'count' => 6], ]); - $results = Pulse::aggregate('slow_request', ['max', 'avg', 'count'], CarbonInterval::hour()); + $results = Pulse::aggregate('slow_request', ['min', 'max', 'sum', 'avg', 'count'], CarbonInterval::hour()); expect($results->all())->toEqual([ - (object) ['key' => 'GET /bar', 'max' => 600, 'avg' => 400, 'count' => 6], - (object) ['key' => 'GET /foo', 'max' => 400, 'avg' => 250, 'count' => 8], + (object) ['key' => 'GET /bar', 'min' => 200, 'max' => 600, 'sum' => 2400, 'avg' => 400, 'count' => 6], + (object) ['key' => 'GET /foo', 'min' => 100, 'max' => 400, 'sum' => 2000, 'avg' => 250, 'count' => 8], ]); }); From a8fb7fae89be215ec5f26d19f7dc0bc3ec936128 Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Sun, 10 Dec 2023 02:17:50 +1100 Subject: [PATCH 056/110] Increase column size (#185) --- .../migrations/2023_06_07_000001_create_pulse_tables.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 41f8520c..156ff225 100644 --- a/database/migrations/2023_06_07_000001_create_pulse_tables.php +++ b/database/migrations/2023_06_07_000001_create_pulse_tables.php @@ -27,13 +27,13 @@ public function up(): void $table->id(); $table->unsignedInteger('timestamp'); $table->string('type'); - $table->text('key'); + $table->mediumText('key'); match ($driver = $connection->getDriverName()) { 'mysql' => $table->char('key_hash', 16)->charset('binary')->virtualAs('unhex(md5(`key`))'), 'pgsql' => $table->uuid('key_hash')->storedAs('md5("key")::uuid'), default => throw new RuntimeException("Unsupported database driver [{$driver}]."), }; - $table->text('value'); + $table->mediumText('value'); $table->index('timestamp'); // For trimming... $table->index('type'); // For fast lookups and purging... @@ -44,7 +44,7 @@ public function up(): void $table->id(); $table->unsignedInteger('timestamp'); $table->string('type'); - $table->text('key'); + $table->mediumText('key'); match ($driver = $connection->getDriverName()) { 'mysql' => $table->char('key_hash', 16)->charset('binary')->virtualAs('unhex(md5(`key`))'), 'pgsql' => $table->uuid('key_hash')->storedAs('md5("key")::uuid'), @@ -63,7 +63,7 @@ public function up(): void $table->unsignedInteger('bucket'); $table->unsignedMediumInteger('period'); $table->string('type'); - $table->text('key'); + $table->mediumText('key'); match ($driver = $connection->getDriverName()) { 'mysql' => $table->char('key_hash', 16)->charset('binary')->virtualAs('unhex(md5(`key`))'), 'pgsql' => $table->uuid('key_hash')->storedAs('md5("key")::uuid'), From 4b424d86977983c661d91ff7963bfdd3f057fe1b Mon Sep 17 00:00:00 2001 From: taylorotwell Date: Sat, 9 Dec 2023 15:18:13 +0000 Subject: [PATCH 057/110] Update facade docblocks --- src/Facades/Pulse.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Facades/Pulse.php b/src/Facades/Pulse.php index c8519678..a1a94e54 100644 --- a/src/Facades/Pulse.php +++ b/src/Facades/Pulse.php @@ -35,10 +35,10 @@ * @method static void trim() * @method static void purge(array $types = null) * @method static \Illuminate\Support\Collection values(string $type, array $keys = null) - * @method static \Illuminate\Support\Collection graph(array $types, string $aggregate, \Carbon\CarbonInterval $interval) - * @method static \Illuminate\Support\Collection aggregate(string $type, array $aggregates, \Carbon\CarbonInterval $interval, string|null $orderBy = null, string $direction = 'desc', int $limit = 101) - * @method static \Illuminate\Support\Collection aggregateTypes(string|array $types, string $aggregate, \Carbon\CarbonInterval $interval, string|null $orderBy = null, string $direction = 'desc', int $limit = 101) - * @method static \Illuminate\Support\Collection aggregateTotal(string|array $types, string $aggregate, \Carbon\CarbonInterval $interval) + * @method static \Illuminate\Support\Collection graph(array $types, $aggregate, \Carbon\CarbonInterval $interval) + * @method static \Illuminate\Support\Collection aggregate(string $type, |array $aggregates, \Carbon\CarbonInterval $interval, string|null $orderBy = null, string $direction = 'desc', int $limit = 101) + * @method static \Illuminate\Support\Collection aggregateTypes(string|array $types, $aggregate, \Carbon\CarbonInterval $interval, string|null $orderBy = null, string $direction = 'desc', int $limit = 101) + * @method static \Illuminate\Support\Collection aggregateTotal(string|array $types, $aggregate, \Carbon\CarbonInterval $interval) * * @see \Laravel\Pulse\Pulse */ From 002e7acef1907462a27d3a72e7209bcb82042de2 Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Sun, 10 Dec 2023 02:20:36 +1100 Subject: [PATCH 058/110] Improve enabled flag (#173) --- src/PulseServiceProvider.php | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/PulseServiceProvider.php b/src/PulseServiceProvider.php index 172d365d..b93131b1 100644 --- a/src/PulseServiceProvider.php +++ b/src/PulseServiceProvider.php @@ -36,10 +36,6 @@ public function register(): void __DIR__.'/../config/pulse.php', 'pulse' ); - if (! $this->app->make('config')->get('pulse.enabled')) { - return; - } - $this->app->singleton(Pulse::class); $this->app->bind(Storage::class, DatabaseStorage::class); @@ -63,15 +59,15 @@ protected function registerIngest(): void */ public function boot(): void { - if (! $this->app->make('config')->get('pulse.enabled')) { - return; + if ($enabled = $this->app->make('config')->get('pulse.enabled')) { + $this->app->make(Pulse::class)->register($this->app->make('config')->get('pulse.recorders')); + $this->listenForEvents(); + } else { + $this->app->make(Pulse::class)->stopRecording(); } - $this->app->make(Pulse::class)->register($this->app->make('config')->get('pulse.recorders')); - $this->registerAuthorization(); $this->registerRoutes(); - $this->listenForEvents(); $this->registerComponents(); $this->registerResources(); $this->registerPublishing(); From 3ca7608fec01105ba1d29fee1516394fabf5c29d Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Mon, 11 Dec 2023 12:53:55 +1100 Subject: [PATCH 059/110] Fix facade --- src/Facades/Pulse.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Facades/Pulse.php b/src/Facades/Pulse.php index a1a94e54..6fa10f59 100644 --- a/src/Facades/Pulse.php +++ b/src/Facades/Pulse.php @@ -12,7 +12,7 @@ * @method static \Laravel\Pulse\Pulse report(\Throwable $e) * @method static \Laravel\Pulse\Pulse startRecording() * @method static \Laravel\Pulse\Pulse stopRecording() - * @method static mixed|null ignore(callable $callback) + * @method static mixed ignore(callable $callback) * @method static \Laravel\Pulse\Pulse flush() * @method static \Laravel\Pulse\Pulse filter(callable $filter) * @method static int store() @@ -22,7 +22,7 @@ * @method static callable authenticatedUserIdResolver() * @method static string|int|null resolveAuthenticatedUserId() * @method static \Laravel\Pulse\Pulse resolveAuthenticatedUserIdUsing(callable $callback) - * @method static mixed|null withUser(\Illuminate\Contracts\Auth\Authenticatable|string|int|null $user, callable $callback) + * @method static mixed withUser(\Illuminate\Contracts\Auth\Authenticatable|string|int|null $user, callable $callback) * @method static \Laravel\Pulse\Pulse rememberUser(\Illuminate\Contracts\Auth\Authenticatable $user) * @method static \Laravel\Pulse\Pulse|string css(string|\Illuminate\Contracts\Support\Htmlable|array|null $css = null) * @method static string js() @@ -35,10 +35,10 @@ * @method static void trim() * @method static void purge(array $types = null) * @method static \Illuminate\Support\Collection values(string $type, array $keys = null) - * @method static \Illuminate\Support\Collection graph(array $types, $aggregate, \Carbon\CarbonInterval $interval) - * @method static \Illuminate\Support\Collection aggregate(string $type, |array $aggregates, \Carbon\CarbonInterval $interval, string|null $orderBy = null, string $direction = 'desc', int $limit = 101) - * @method static \Illuminate\Support\Collection aggregateTypes(string|array $types, $aggregate, \Carbon\CarbonInterval $interval, string|null $orderBy = null, string $direction = 'desc', int $limit = 101) - * @method static \Illuminate\Support\Collection aggregateTotal(string|array $types, $aggregate, \Carbon\CarbonInterval $interval) + * @method static \Illuminate\Support\Collection graph(array $types, string $aggregate, \Carbon\CarbonInterval $interval) + * @method static \Illuminate\Support\Collection aggregate(string $type, string|array $aggregates, \Carbon\CarbonInterval $interval, string|null $orderBy = null, string $direction = 'desc', int $limit = 101) + * @method static \Illuminate\Support\Collection aggregateTypes(string|array $types, string $aggregate, \Carbon\CarbonInterval $interval, string|null $orderBy = null, string $direction = 'desc', int $limit = 101) + * @method static \Illuminate\Support\Collection aggregateTotal(string|array $types, string $aggregate, \Carbon\CarbonInterval $interval) * * @see \Laravel\Pulse\Pulse */ From 3e540495fe66616f91625888fe643c5a57adf08e Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Mon, 11 Dec 2023 12:57:14 +1100 Subject: [PATCH 060/110] Static analysis --- src/Livewire/Usage.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Livewire/Usage.php b/src/Livewire/Usage.php index e9bf4596..5e30065e 100644 --- a/src/Livewire/Usage.php +++ b/src/Livewire/Usage.php @@ -50,7 +50,7 @@ function () use ($type) { 'slow_requests' => 'slow_user_request', 'jobs' => 'user_job', }, - 'count', // @phpstan-ignore argument.type + 'count', $this->periodAsInterval(), limit: 10, ); From 7ec3524975afd6ae2e654d50d1ac5e04f3ad8ec5 Mon Sep 17 00:00:00 2001 From: Jess Archer Date: Tue, 12 Dec 2023 02:18:19 +1000 Subject: [PATCH 061/110] [1.x] Chart Improvements (#206) * Extract chart components * Update minimum Livewire version * Fix scaling --- composer.json | 2 +- resources/views/livewire/queues.blade.php | 247 ++++++----- resources/views/livewire/servers.blade.php | 468 +++++++++++---------- src/Livewire/Queues.php | 4 +- src/Livewire/Servers.php | 4 +- 5 files changed, 384 insertions(+), 341 deletions(-) diff --git a/composer.json b/composer.json index 9f0301c9..75436910 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,7 @@ "illuminate/routing": "^10.21", "illuminate/support": "^10.21", "illuminate/view": "^10.21", - "livewire/livewire": "^3.02", + "livewire/livewire": "^3.2", "nesbot/carbon": "^2.67" }, "require-dev": { diff --git a/resources/views/livewire/queues.blade.php b/resources/views/livewire/queues.blade.php index 00357645..d3278ace 100644 --- a/resources/views/livewire/queues.blade.php +++ b/resources/views/livewire/queues.blade.php @@ -66,120 +66,11 @@

@@ -190,3 +81,131 @@ class="h-14" @endif + +@script + +@endscript diff --git a/resources/views/livewire/servers.blade.php b/resources/views/livewire/servers.blade.php index 18595ad0..a0b9a065 100644 --- a/resources/views/livewire/servers.blade.php +++ b/resources/views/livewire/servers.blade.php @@ -60,85 +60,11 @@ class="overflow-x-auto pb-px default:col-span-full default:lg:col-span-{{ $cols
@@ -157,85 +83,12 @@ class="w-full min-w-[5rem] max-w-xs h-9 relative"
@@ -250,70 +103,12 @@ class="w-full min-w-[5rem] max-w-xs h-9 relative"
@@ -324,3 +119,232 @@ class="w-full min-w-[5rem] max-w-xs h-9 relative"
@endif + +@script + +@endscript diff --git a/src/Livewire/Queues.php b/src/Livewire/Queues.php index bc38271e..9a2988b5 100644 --- a/src/Livewire/Queues.php +++ b/src/Livewire/Queues.php @@ -4,12 +4,12 @@ use Illuminate\Contracts\Support\Renderable; use Illuminate\Support\Facades\Config; -use Illuminate\Support\Facades\Request; use Illuminate\Support\Facades\View; use Illuminate\Support\Str; use Laravel\Pulse\Facades\Pulse; use Laravel\Pulse\Recorders\Queues as QueuesRecorder; use Livewire\Attributes\Lazy; +use Livewire\Livewire; /** * @internal @@ -30,7 +30,7 @@ public function render(): Renderable $this->periodAsInterval(), )); - if (Request::hasHeader('X-Livewire')) { + if (Livewire::isLivewireRequest()) { $this->dispatch('queues-chart-update', queues: $queues); } diff --git a/src/Livewire/Servers.php b/src/Livewire/Servers.php index bea2af98..cba221ba 100644 --- a/src/Livewire/Servers.php +++ b/src/Livewire/Servers.php @@ -4,10 +4,10 @@ use Carbon\CarbonImmutable; use Illuminate\Contracts\Support\Renderable; -use Illuminate\Support\Facades\Request; use Illuminate\Support\Facades\View; use Laravel\Pulse\Facades\Pulse; use Livewire\Attributes\Lazy; +use Livewire\Livewire; /** * @internal @@ -43,7 +43,7 @@ public function render(): Renderable }); }); - if (Request::hasHeader('X-Livewire')) { + if (Livewire::isLivewireRequest()) { $this->dispatch('servers-chart-update', servers: $servers); } From aae03718af429adddf571b10638ca9025b21812d Mon Sep 17 00:00:00 2001 From: Salah Kanjo <121197517+sakanjo@users.noreply.github.com> Date: Mon, 11 Dec 2023 19:20:37 +0300 Subject: [PATCH 062/110] [1.x] Fix avatar size (#179) * style: use classes instead of attributes * Prevent stretching avatars --------- Co-authored-by: Jess Archer --- resources/views/livewire/usage.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/livewire/usage.blade.php b/resources/views/livewire/usage.blade.php index e3e1b054..e53b68f1 100644 --- a/resources/views/livewire/usage.blade.php +++ b/resources/views/livewire/usage.blade.php @@ -43,7 +43,7 @@ class="flex-1" @if ($userRequestCount->user->avatar ?? false) - + @endif From ddad55197fc00c22a560ed04f92ae1624653295d Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Tue, 12 Dec 2023 10:08:03 +1100 Subject: [PATCH 063/110] Static analysis --- src/Recorders/UserJobs.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Recorders/UserJobs.php b/src/Recorders/UserJobs.php index 4e212dab..9a335354 100644 --- a/src/Recorders/UserJobs.php +++ b/src/Recorders/UserJobs.php @@ -45,7 +45,7 @@ public function record(JobQueued $event): void [$timestamp, $name, $userIdResolver] = [ CarbonImmutable::now()->getTimestamp(), match (true) { - is_string($event->job) => $event->job, + is_string($name = $event->job) => $name, method_exists($event->job, 'displayName') => $event->job->displayName(), default => $event->job::class, }, From e471b1f6cb81476a1bc780f9685ad4d39a66a93b Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Tue, 12 Dec 2023 11:04:52 +1100 Subject: [PATCH 064/110] Update upgrade guide --- UPGRADE.MD | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/UPGRADE.MD b/UPGRADE.MD index 2934af35..1794e431 100644 --- a/UPGRADE.MD +++ b/UPGRADE.MD @@ -2,5 +2,12 @@ # Beta to 1.x -- [Auto-incrementing IDs were added to Pulse's tables](https://github.com/laravel/pulse/pull/142). This is recommended if you are using a configuration that requires tables to have a unique key on every table, e.g., PlanetScale. +## Required + +- [Added a `highlight` configuration option to `pulse.recorders.SlowQueries` configuration block](https://github.com/laravel/pulse/pull/185). +## Optional + +- [Auto-incrementing IDs were added to Pulse's tables](https://github.com/laravel/pulse/pull/142). This is recommended if you are using a configuration that requires tables to have a unique key on every table, e.g., PlanetScale. +- [The TEXT columns were made MEDIUMTEXT columns in the `pulse_` tables](https://github.com/laravel/pulse/pull/185). Recommend to support longer content values, such as long SQL queries. +- [Pulse's migrations are now published to the application](https://github.com/laravel/pulse/pull/81). Recommend so you can have complete control over the migrations as needed. From 7d13da1bbb2041653057b3406a72e0c8baa22514 Mon Sep 17 00:00:00 2001 From: Ashley Shenton Date: Tue, 12 Dec 2023 07:20:22 +0000 Subject: [PATCH 065/110] Update highlight slow queries link --- UPGRADE.MD | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UPGRADE.MD b/UPGRADE.MD index 1794e431..7df192db 100644 --- a/UPGRADE.MD +++ b/UPGRADE.MD @@ -4,7 +4,7 @@ ## Required -- [Added a `highlight` configuration option to `pulse.recorders.SlowQueries` configuration block](https://github.com/laravel/pulse/pull/185). +- [Added a `highlight` configuration option to `pulse.recorders.SlowQueries` configuration block](https://github.com/laravel/pulse/pull/172). ## Optional From 489bca25f1820e5c45f1e89de00d29adecb39e38 Mon Sep 17 00:00:00 2001 From: Jess Archer Date: Wed, 13 Dec 2023 00:56:35 +1000 Subject: [PATCH 066/110] Handle `null` cache keys (#216) --- src/Recorders/CacheInteractions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Recorders/CacheInteractions.php b/src/Recorders/CacheInteractions.php index b09664b1..869bfbf9 100644 --- a/src/Recorders/CacheInteractions.php +++ b/src/Recorders/CacheInteractions.php @@ -43,7 +43,7 @@ public function record(CacheHit|CacheMissed $event): void [$timestamp, $class, $key] = [ CarbonImmutable::now()->getTimestamp(), $event::class, - $event->key, + (string) $event->key, ]; $this->pulse->lazy(function () use ($timestamp, $class, $key) { From 85072690373a0ee88347710f8fa5d475dc60468e Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Wed, 13 Dec 2023 02:03:02 +1100 Subject: [PATCH 067/110] Bail on store when entries is empty (#212) --- src/Pulse.php | 48 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/src/Pulse.php b/src/Pulse.php index 25c8d686..54c65a9a 100644 --- a/src/Pulse.php +++ b/src/Pulse.php @@ -129,18 +129,16 @@ public function register(array $recorders): self ->filter(fn ($recorder) => $recorder->listen ?? null) ->each(fn ($recorder) => $event->listen( $recorder->listen, - fn ($event) => $this->rescue(fn () => Collection::wrap($recorder->record($event))) + fn ($event) => $this->rescue(fn () => $recorder->record($event)) )) ); $recorders ->filter(fn ($recorder) => method_exists($recorder, 'register')) ->each(function ($recorder) { - $record = function (...$args) use ($recorder) { - $this->rescue(fn () => Collection::wrap($recorder->record(...$args))); - }; - - $this->app->call($recorder->register(...), ['record' => $record]); + $this->app->call($recorder->register(...), [ + 'record' => fn (...$args) => $this->rescue(fn () => $recorder->record(...$args)), + ]); }); $this->recorders = collect([...$this->recorders, ...$recorders]); @@ -267,8 +265,11 @@ public function ignore($callback): mixed public function flush(): self { $this->entries = collect([]); + $this->lazy = collect([]); + $this->rememberedUserId = null; + return $this; } @@ -289,21 +290,33 @@ public function filter(callable $filter): self */ public function store(): int { + $entries = $this->rescue(function () { + $this->lazy->each(fn ($lazy) => $lazy()); + + return $this->entries->filter($this->shouldRecord(...)); + }) ?? collect([]); + + if ($entries->isEmpty()) { + $this->flush(); + + return 0; + } + $ingest = $this->app->make(Ingest::class); - $this->lazy->each(fn ($lazy) => $lazy()); + $count = $this->rescue(function () use ($entries, $ingest) { + $ingest->ingest($entries); - $this->rescue(fn () => $ingest->ingest( - $this->entries->filter($this->shouldRecord(...)), - )); + return $entries->count(); + }) ?? 0; Lottery::odds(...$this->app->make('config')->get('pulse.ingest.trim_lottery')) ->winner(fn () => $this->rescue($ingest->trim(...))) ->choose(); - $this->rememberedUserId = null; + $this->flush(); - return tap($this->entries->count(), $this->flush(...)); + return $count; } /** @@ -508,15 +521,20 @@ public function handleExceptionsUsing(callable $callback): self /** * Execute the given callback handling any exceptions. * - * @param (callable(): mixed) $callback + * @template TReturn + * + * @param (callable(): TReturn) $callback + * @return TReturn|null */ - public function rescue(callable $callback): void + public function rescue(callable $callback): mixed { try { - $callback(); + return $callback(); } catch (Throwable $e) { ($this->handleExceptionsUsing ?? fn () => null)($e); } + + return null; } /** From c4af3306980e49436a0aa595686c770cd52f3620 Mon Sep 17 00:00:00 2001 From: taylorotwell Date: Tue, 12 Dec 2023 15:03:32 +0000 Subject: [PATCH 068/110] Update facade docblocks --- src/Facades/Pulse.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Facades/Pulse.php b/src/Facades/Pulse.php index 6fa10f59..362a27ed 100644 --- a/src/Facades/Pulse.php +++ b/src/Facades/Pulse.php @@ -29,7 +29,7 @@ * @method static bool registersRoutes() * @method static \Laravel\Pulse\Pulse ignoreRoutes() * @method static \Laravel\Pulse\Pulse handleExceptionsUsing(callable $callback) - * @method static void rescue(callable $callback) + * @method static |null rescue(callable $callback) * @method static \Laravel\Pulse\Pulse setContainer(\Illuminate\Contracts\Foundation\Application $container) * @method static void afterResolving(\Illuminate\Contracts\Foundation\Application $app, string $class, \Closure $callback) * @method static void trim() From e4119446f97d11cde0d330c21011c0e096301b64 Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Wed, 13 Dec 2023 03:29:59 +1100 Subject: [PATCH 069/110] [1.x] Normalize SQS queue names (#187) * Normalize SQS queue names * Fix code styling * Lint * Return queue --------- Co-authored-by: timacdonald --- src/Recorders/Queues.php | 54 ++++++++++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 10 deletions(-) diff --git a/src/Recorders/Queues.php b/src/Recorders/Queues.php index b148f6ab..b9765288 100644 --- a/src/Recorders/Queues.php +++ b/src/Recorders/Queues.php @@ -50,18 +50,22 @@ public function record(JobReleasedAfterException|JobFailed|JobProcessed|JobProce return; } - [$timestamp, $class, $connection, $uuid, $name] = [ + [$timestamp, $class, $connection, $queue, $uuid, $name] = [ CarbonImmutable::now()->getTimestamp(), - $event::class, - match ($event::class) { - JobQueued::class => $event->connectionName.':'.($event->job->queue ?? $this->getDefaultQueue($event->connectionName)), - default => $event->job->getConnectionName().':'.$event->job->getQueue(), // @phpstan-ignore method.nonObject method.nonObject + $class = $event::class, + match ($class) { + JobQueued::class => $event->connectionName, + default => $event->job->getConnectionName(), // @phpstan-ignore method.nonObject }, - match ($event::class) { - JobQueued::class => $event->payload()['uuid'], + match ($class) { + JobQueued::class => $event->job->queue ?? null, + default => $event->job->getQueue(), // @phpstan-ignore method.nonObject + }, + match ($class) { + JobQueued::class => $event->payload()['uuid'], // @phpstan-ignore method.notFound default => $event->job->uuid(), // @phpstan-ignore method.nonObject }, - match ($event::class) { + match ($class) { JobQueued::class => match (true) { is_string($event->job) => $event->job, method_exists($event->job, 'displayName') => $event->job->displayName(), @@ -71,11 +75,15 @@ public function record(JobReleasedAfterException|JobFailed|JobProcessed|JobProce }, ]; - $this->pulse->lazy(function () use ($timestamp, $class, $uuid, $name, $connection) { + $this->pulse->lazy(function () use ($timestamp, $class, $connection, $queue, $uuid, $name) { if (! $this->shouldSampleDeterministically($uuid) || $this->shouldIgnore($name)) { return; } + $queue = $queue === null + ? $this->getDefaultQueue($connection) + : $this->normalizeSqsQueue($connection, $queue); + $this->pulse->record( type: match ($class) { // @phpstan-ignore match.unhandled JobQueued::class => 'queued', @@ -84,7 +92,7 @@ public function record(JobReleasedAfterException|JobFailed|JobProcessed|JobProce JobReleasedAfterException::class => 'released', JobFailed::class => 'failed', }, - key: $connection, + key: "{$connection}:{$queue}", timestamp: $timestamp, )->count()->onlyBuckets(); }); @@ -97,4 +105,30 @@ protected function getDefaultQueue(string $connection): string { return $this->config->get('queue.connections.'.$connection.'.queue', 'default'); } + + /** + * Normalize the SQS queue name. + */ + protected function normalizeSqsQueue(string $connection, string $queue): string + { + $config = $this->config->get("queue.connections.{$connection}") ?? []; + + if (($config['driver'] ?? null) !== 'sqs') { + return $queue; + } + + if ($config['prefix'] ?? null) { + $prefix = preg_quote($config['prefix'], '#'); + + $queue = preg_replace("#^{$prefix}/#", '', $queue) ?? $queue; + } + + if ($config['suffix'] ?? null) { + $suffix = preg_quote($config['suffix'], '#'); + + $queue = preg_replace("#{$suffix}$#", '', $queue) ?? $queue; + } + + return $queue; + } } From 126fd8888d9e2acc58be4bc75d55c3bbe507d0a2 Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Wed, 13 Dec 2023 04:17:41 +1100 Subject: [PATCH 070/110] [1.x] Allow configurable trim length (#184) * Allow configurable trim length * Upgrade guide * Add v1 todos --------- Co-authored-by: James Brooks --- UPGRADE.MD | 4 +++- config/pulse.php | 5 ++++- src/Ingests/RedisIngest.php | 11 +++++++++-- src/Livewire/SlowQueries.php | 1 + src/Pulse.php | 5 ++++- tests/Feature/RedisTest.php | 22 +++++++++++++++++++++- 6 files changed, 42 insertions(+), 6 deletions(-) diff --git a/UPGRADE.MD b/UPGRADE.MD index 7df192db..637c8b09 100644 --- a/UPGRADE.MD +++ b/UPGRADE.MD @@ -4,7 +4,9 @@ ## Required -- [Added a `highlight` configuration option to `pulse.recorders.SlowQueries` configuration block](https://github.com/laravel/pulse/pull/172). +- [Added a `pulse.recorders.SlowQueries.highlight` configuration option](https://github.com/laravel/pulse/pull/185). You should update your configuration to match. +- [`pulse.ingest.trim_lottery` configuration key was renamed to `pulse.ingest.trim.lottery`](https://github.com/laravel/pulse/pull/184). You should update your configuration to match. +- [Added a `pulse.ingest.trim.keep` configuration option](https://github.com/laravel/pulse/pull/184). You should update your configuration to match. ## Optional diff --git a/config/pulse.php b/config/pulse.php index d998642b..a52f5c31 100644 --- a/config/pulse.php +++ b/config/pulse.php @@ -78,7 +78,10 @@ 'ingest' => [ 'driver' => env('PULSE_INGEST_DRIVER', 'storage'), - 'trim_lottery' => [1, 1_000], + 'trim' => [ + 'lottery' => [1, 1_000], + 'keep' => '7 days', + ], 'redis' => [ 'connection' => env('PULSE_REDIS_CONNECTION'), diff --git a/src/Ingests/RedisIngest.php b/src/Ingests/RedisIngest.php index b2eec379..809468e1 100644 --- a/src/Ingests/RedisIngest.php +++ b/src/Ingests/RedisIngest.php @@ -3,6 +3,7 @@ namespace Laravel\Pulse\Ingests; use Carbon\CarbonImmutable; +use Carbon\CarbonInterval; use Illuminate\Config\Repository; use Illuminate\Redis\RedisManager; use Illuminate\Support\Collection; @@ -55,11 +56,17 @@ public function ingest(Collection $items): void */ public function trim(): void { + $keep = $this->config->get('pulse.ingest.trim.keep'); + $this->connection()->xtrim( $this->stream, - 'MINID', + is_int($keep) ? 'MAXLEN' : 'MINID', '~', - CarbonImmutable::now()->subWeek()->getTimestampMs() + is_int($keep) + ? $keep + : CarbonImmutable::now()->subMilliseconds( + (int) CarbonInterval::fromString($keep)->totalMilliseconds + )->getTimestampMs(), ); } diff --git a/src/Livewire/SlowQueries.php b/src/Livewire/SlowQueries.php index 41773c24..78df9a1b 100644 --- a/src/Livewire/SlowQueries.php +++ b/src/Livewire/SlowQueries.php @@ -57,6 +57,7 @@ public function render(): Renderable 'time' => $time, 'runAt' => $runAt, 'config' => [ + // TODO remove fallback when tagging v1 'highlighting' => true, ...Config::get('pulse.recorders.'.SlowQueriesRecorder::class), ], diff --git a/src/Pulse.php b/src/Pulse.php index 54c65a9a..4477530d 100644 --- a/src/Pulse.php +++ b/src/Pulse.php @@ -310,7 +310,10 @@ public function store(): int return $entries->count(); }) ?? 0; - Lottery::odds(...$this->app->make('config')->get('pulse.ingest.trim_lottery')) + // TODO remove fallback when tagging v1 + $odds = $this->app->make('config')->get('pulse.ingest.trim.lottery') ?? $this->app->make('config')->get('pulse.ingest.trim_lottery'); + + Lottery::odds(...$odds) ->winner(fn () => $this->rescue($ingest->trim(...))) ->choose(); diff --git a/tests/Feature/RedisTest.php b/tests/Feature/RedisTest.php index 2beb2dcb..24ab03f2 100644 --- a/tests/Feature/RedisTest.php +++ b/tests/Feature/RedisTest.php @@ -32,7 +32,7 @@ expect($commands)->toContain('"XADD" "laravel_database_laravel:pulse:ingest" "*" "data" "O:19:\"Laravel\\\\Pulse\\\\Entry\\":6:{s:15:\"\x00*\x00aggregations\";a:0:{}s:14:\"\x00*\x00onlyBuckets\";b:0;s:9:\"timestamp\";i:1700752211;s:4:\"type\";s:3:\"foo\";s:3:\"key\";s:3:\"bar\";s:5:\"value\";i:123;}"'); })->with(['predis', 'phpredis']); -it('runs the same commands while triming the stream', function ($driver) { +it('keeps 7 days of data, by default, when trimming', function ($driver) { Config::set('database.redis.client', $driver); Date::setTestNow(Date::parse('2000-01-02 03:04:05')->startOfSecond()); @@ -41,6 +41,26 @@ expect($commands)->toContain('"XTRIM" "laravel_database_laravel:pulse:ingest" "MINID" "~" "946177445000"'); })->with(['predis', 'phpredis']); +it('can configure days of data to keep when trimming', function ($driver) { + Config::set('database.redis.client', $driver); + Date::setTestNow(Date::parse('2000-01-02 03:04:05')->startOfSecond()); + Config::set('pulse.ingest.trim.keep', '1 day'); + + $commands = captureRedisCommands(fn () => App::make(RedisIngest::class)->trim()); + + expect($commands)->toContain('"XTRIM" "laravel_database_laravel:pulse:ingest" "MINID" "~" "946695845000"'); +})->with(['predis', 'phpredis']); + +it('can configure the number of entries to keep when trimming', function ($driver) { + Config::set('database.redis.client', $driver); + Date::setTestNow(Date::parse('2000-01-02 03:04:05')->startOfSecond()); + Config::set('pulse.ingest.trim.keep', 54321); + + $commands = captureRedisCommands(fn () => App::make(RedisIngest::class)->trim()); + + expect($commands)->toContain('"XTRIM" "laravel_database_laravel:pulse:ingest" "MAXLEN" "~" "54321"'); +})->with(['predis', 'phpredis']); + it('runs the same commands while storing', function ($driver) { Config::set('database.redis.client', $driver); Config::set('pulse.ingest.redis.chunk', 567); From 5236579a46b667ebe45011c4da05d7bdea71371b Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Wed, 13 Dec 2023 10:52:53 +1100 Subject: [PATCH 071/110] Fix facade --- src/Facades/Pulse.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Facades/Pulse.php b/src/Facades/Pulse.php index 362a27ed..5fd78168 100644 --- a/src/Facades/Pulse.php +++ b/src/Facades/Pulse.php @@ -29,7 +29,7 @@ * @method static bool registersRoutes() * @method static \Laravel\Pulse\Pulse ignoreRoutes() * @method static \Laravel\Pulse\Pulse handleExceptionsUsing(callable $callback) - * @method static |null rescue(callable $callback) + * @method static mixed rescue(callable $callback) * @method static \Laravel\Pulse\Pulse setContainer(\Illuminate\Contracts\Foundation\Application $container) * @method static void afterResolving(\Illuminate\Contracts\Foundation\Application $app, string $class, \Closure $callback) * @method static void trim() From 1f2d6667b4a68ea4780a149677757193ed692702 Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Wed, 13 Dec 2023 11:05:37 +1100 Subject: [PATCH 072/110] Fix flaky tests --- tests/Pest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Pest.php b/tests/Pest.php index f7682b9a..66aa0b9c 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -33,6 +33,7 @@ Pulse::flush(); Pulse::handleExceptionsUsing(fn (Throwable $e) => throw $e); Gate::define('viewPulse', fn ($user = null) => true); + Config::set('pulse.ingest.trim.lottery', [0, 1]); }) ->afterEach(function () { Str::createUuidsNormally(); From 9da330fe6675f6e59a6109f768a9e799b61341e8 Mon Sep 17 00:00:00 2001 From: Jess Archer Date: Wed, 13 Dec 2023 12:04:44 +1000 Subject: [PATCH 073/110] Fix passing of `Htmlable` class without `__toString` method --- src/Pulse.php | 2 +- tests/Feature/Livewire/CustomCardTest.php | 30 ++++++++++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/Pulse.php b/src/Pulse.php index 4477530d..f49aa130 100644 --- a/src/Pulse.php +++ b/src/Pulse.php @@ -461,7 +461,7 @@ public function rememberUser(Authenticatable $user): self public function css(string|Htmlable|array|null $css = null): string|self { if (func_num_args() === 1) { - $this->css = array_values(array_unique(array_merge($this->css, Arr::wrap($css)))); + $this->css = array_values(array_unique(array_merge($this->css, Arr::wrap($css)), SORT_REGULAR)); return $this; } diff --git a/tests/Feature/Livewire/CustomCardTest.php b/tests/Feature/Livewire/CustomCardTest.php index d17b0177..7c3540c4 100644 --- a/tests/Feature/Livewire/CustomCardTest.php +++ b/tests/Feature/Livewire/CustomCardTest.php @@ -1,5 +1,6 @@ assertOk(); + + $css = Pulse::css(); + + expect($css)->toContain(''); +}); + it('loads custom css using a Htmlable', function () { Livewire::test(CustomCardWithCssHtmlable::class) ->assertOk(); @@ -41,7 +51,7 @@ protected function css() } } -class CustomCardWithCssHtmlable extends Card +class CustomCardWithCssHtmlString extends Card { public function render() { @@ -53,3 +63,21 @@ protected function css() return new HtmlString(''); } } + +class CustomCardWithCssHtmlable extends Card +{ + public function render() + { + return '
'; + } + + protected function css() + { + return new class implements Htmlable { + public function toHtml() + { + return ''; + } + }; + } +} From 047161e018a7927c0b4357b6be9a591012722287 Mon Sep 17 00:00:00 2001 From: jessarcher Date: Wed, 13 Dec 2023 02:05:17 +0000 Subject: [PATCH 074/110] Fix code styling --- tests/Feature/Livewire/CustomCardTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/Feature/Livewire/CustomCardTest.php b/tests/Feature/Livewire/CustomCardTest.php index 7c3540c4..6e8bc5db 100644 --- a/tests/Feature/Livewire/CustomCardTest.php +++ b/tests/Feature/Livewire/CustomCardTest.php @@ -73,7 +73,8 @@ public function render() protected function css() { - return new class implements Htmlable { + return new class implements Htmlable + { public function toHtml() { return ''; From bfd305bb3f2cee9c43570fce0504835d7526ce8d Mon Sep 17 00:00:00 2001 From: Jess Archer Date: Thu, 14 Dec 2023 00:39:39 +1000 Subject: [PATCH 075/110] Add card helper methods for retrieving data (#220) --- src/Contracts/Storage.php | 17 ++---- src/Livewire/Cache.php | 9 +--- src/Livewire/Card.php | 77 +++++++++++++++++++++++++++ src/Livewire/Exceptions.php | 6 +-- src/Livewire/Queues.php | 6 +-- src/Livewire/Servers.php | 7 +-- src/Livewire/SlowJobs.php | 6 +-- src/Livewire/SlowOutgoingRequests.php | 6 +-- src/Livewire/SlowQueries.php | 6 +-- src/Livewire/SlowRequests.php | 6 +-- src/Livewire/Usage.php | 5 +- src/Storage/DatabaseStorage.php | 18 +++---- 12 files changed, 99 insertions(+), 70 deletions(-) diff --git a/src/Contracts/Storage.php b/src/Contracts/Storage.php index 7e8cbf09..980a0c70 100644 --- a/src/Contracts/Storage.php +++ b/src/Contracts/Storage.php @@ -30,18 +30,11 @@ public function purge(?array $types = null): void; * Retrieve values for the given type. * * @param list $keys - * @return \Illuminate\Support\Collection< - * int, - * array< - * string, - * array{ - * timestamp: int, - * type: string, - * key: string, - * value: string - * } - * > - * > + * @return \Illuminate\Support\Collection */ public function values(string $type, ?array $keys = null): Collection; diff --git a/src/Livewire/Cache.php b/src/Livewire/Cache.php index 0509d89d..1960ede7 100644 --- a/src/Livewire/Cache.php +++ b/src/Livewire/Cache.php @@ -5,7 +5,6 @@ use Illuminate\Contracts\Support\Renderable; use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\View; -use Laravel\Pulse\Facades\Pulse; use Laravel\Pulse\Recorders\CacheInteractions as CacheInteractionsRecorder; use Livewire\Attributes\Lazy; @@ -24,11 +23,7 @@ public function render(): Renderable { [$cacheInteractions, $allTime, $allRunAt] = $this->remember( fn () => with( - Pulse::aggregateTotal( - ['cache_hit', 'cache_miss'], - 'count', - $this->periodAsInterval(), - ), + $this->aggregateTotal(['cache_hit', 'cache_miss'], 'count'), fn ($results) => (object) [ 'hits' => $results['cache_hit'] ?? 0, 'misses' => $results['cache_miss'] ?? 0, @@ -38,7 +33,7 @@ public function render(): Renderable ); [$cacheKeyInteractions, $keyTime, $keyRunAt] = $this->remember( - fn () => Pulse::aggregateTypes(['cache_hit', 'cache_miss'], 'count', $this->periodAsInterval()) + fn () => $this->aggregateTypes(['cache_hit', 'cache_miss'], 'count') ->map(function ($row) { return (object) [ 'key' => $row->key, diff --git a/src/Livewire/Card.php b/src/Livewire/Card.php index 3b3036a4..0d719b39 100644 --- a/src/Livewire/Card.php +++ b/src/Livewire/Card.php @@ -3,6 +3,7 @@ namespace Laravel\Pulse\Livewire; use Illuminate\Contracts\Support\Renderable; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\View; use Laravel\Pulse\Facades\Pulse; use Livewire\Component; @@ -10,6 +11,8 @@ abstract class Card extends Component { + use Concerns\HasPeriod, Concerns\RemembersQueries; + /** * The number of columns to span. * @@ -69,4 +72,78 @@ protected function css() { return null; } + + /** + * Retrieve values for the given type. + * + * @param list $keys + * @return \Illuminate\Support\Collection + */ + protected function values(string $type, ?array $keys = null): Collection + { + return Pulse::values($type, $keys); + } + + /** + * Retrieve aggregate values for plotting on a graph. + * + * @param list $types + * @param 'count'|'min'|'max'|'sum'|'avg' $aggregate + * @return \Illuminate\Support\Collection>> + */ + protected function graph(array $types, string $aggregate): Collection + { + return Pulse::graph($types, $aggregate, $this->periodAsInterval()); + } + + /** + * Retrieve aggregate values for the given type. + * + * @param 'count'|'min'|'max'|'sum'|'avg'|list<'count'|'min'|'max'|'sum'|'avg'> $aggregates + * @return \Illuminate\Support\Collection + */ + protected function aggregate( + string $type, + string|array $aggregates, + ?string $orderBy = null, + string $direction = 'desc', + int $limit = 101, + ): Collection { + return Pulse::aggregate($type, $aggregates, $this->periodAsInterval(), $orderBy, $direction, $limit); + } + + /** + * Retrieve aggregate values for the given types. + * + * @param string|list $types + * @param 'count'|'min'|'max'|'sum'|'avg' $aggregate + * @return \Illuminate\Support\Collection + */ + protected function aggregateTypes( + string|array $types, + string $aggregate, + ?string $orderBy = null, + string $direction = 'desc', + int $limit = 101, + ): Collection { + return Pulse::aggregateTypes($types, $aggregate, $this->periodAsInterval(), $orderBy, $direction, $limit); + } + + /** + * Retrieve an aggregate total for the given types. + * + * @param string|list $types + * @param 'count'|'min'|'max'|'sum'|'avg' $aggregate + * @return \Illuminate\Support\Collection + */ + protected function aggregateTotal( + array|string $types, + string $aggregate, + ): Collection { + return Pulse::aggregateTotal($types, $aggregate, $this->periodAsInterval()); + } } diff --git a/src/Livewire/Exceptions.php b/src/Livewire/Exceptions.php index 919887ee..feb42342 100644 --- a/src/Livewire/Exceptions.php +++ b/src/Livewire/Exceptions.php @@ -6,7 +6,6 @@ use Illuminate\Contracts\Support\Renderable; use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\View; -use Laravel\Pulse\Facades\Pulse; use Laravel\Pulse\Recorders\Exceptions as ExceptionsRecorder; use Livewire\Attributes\Lazy; use Livewire\Attributes\Url; @@ -17,8 +16,6 @@ #[Lazy] class Exceptions extends Card { - use Concerns\HasPeriod, Concerns\RemembersQueries; - /** * Ordering. * @@ -33,10 +30,9 @@ class Exceptions extends Card public function render(): Renderable { [$exceptions, $time, $runAt] = $this->remember( - fn () => Pulse::aggregate( + fn () => $this->aggregate( 'exception', ['max', 'count'], - $this->periodAsInterval(), match ($this->orderBy) { 'latest' => 'max', default => 'count' diff --git a/src/Livewire/Queues.php b/src/Livewire/Queues.php index 9a2988b5..2a113316 100644 --- a/src/Livewire/Queues.php +++ b/src/Livewire/Queues.php @@ -6,7 +6,6 @@ use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\View; use Illuminate\Support\Str; -use Laravel\Pulse\Facades\Pulse; use Laravel\Pulse\Recorders\Queues as QueuesRecorder; use Livewire\Attributes\Lazy; use Livewire\Livewire; @@ -17,17 +16,14 @@ #[Lazy] class Queues extends Card { - use Concerns\HasPeriod, Concerns\RemembersQueries; - /** * Render the component. */ public function render(): Renderable { - [$queues, $time, $runAt] = $this->remember(fn () => Pulse::graph( + [$queues, $time, $runAt] = $this->remember(fn () => $this->graph( ['queued', 'processing', 'processed', 'released', 'failed'], 'count', - $this->periodAsInterval(), )); if (Livewire::isLivewireRequest()) { diff --git a/src/Livewire/Servers.php b/src/Livewire/Servers.php index cba221ba..e52db790 100644 --- a/src/Livewire/Servers.php +++ b/src/Livewire/Servers.php @@ -5,7 +5,6 @@ use Carbon\CarbonImmutable; use Illuminate\Contracts\Support\Renderable; use Illuminate\Support\Facades\View; -use Laravel\Pulse\Facades\Pulse; use Livewire\Attributes\Lazy; use Livewire\Livewire; @@ -15,17 +14,15 @@ #[Lazy] class Servers extends Card { - use Concerns\HasPeriod, Concerns\RemembersQueries; - /** * Render the component. */ public function render(): Renderable { [$servers, $time, $runAt] = $this->remember(function () { - $graphs = Pulse::graph(['cpu', 'memory'], 'avg', $this->periodAsInterval()); + $graphs = $this->graph(['cpu', 'memory'], 'avg'); - return Pulse::values('system') + return $this->values('system') ->map(function ($system, $slug) use ($graphs) { $values = json_decode($system->value, flags: JSON_THROW_ON_ERROR); diff --git a/src/Livewire/SlowJobs.php b/src/Livewire/SlowJobs.php index 01614b25..492ba399 100644 --- a/src/Livewire/SlowJobs.php +++ b/src/Livewire/SlowJobs.php @@ -5,7 +5,6 @@ use Illuminate\Contracts\Support\Renderable; use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\View; -use Laravel\Pulse\Facades\Pulse; use Laravel\Pulse\Recorders\SlowJobs as SlowJobsRecorder; use Livewire\Attributes\Lazy; use Livewire\Attributes\Url; @@ -16,8 +15,6 @@ #[Lazy] class SlowJobs extends Card { - use Concerns\HasPeriod, Concerns\RemembersQueries; - /** * Ordering. * @@ -32,10 +29,9 @@ class SlowJobs extends Card public function render(): Renderable { [$slowJobs, $time, $runAt] = $this->remember( - fn () => Pulse::aggregate( + fn () => $this->aggregate( 'slow_job', ['max', 'count'], - $this->periodAsInterval(), match ($this->orderBy) { 'count' => 'count', default => 'max', diff --git a/src/Livewire/SlowOutgoingRequests.php b/src/Livewire/SlowOutgoingRequests.php index dfcec094..da1cfbfc 100644 --- a/src/Livewire/SlowOutgoingRequests.php +++ b/src/Livewire/SlowOutgoingRequests.php @@ -5,7 +5,6 @@ use Illuminate\Contracts\Support\Renderable; use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\View; -use Laravel\Pulse\Facades\Pulse; use Laravel\Pulse\Recorders\SlowOutgoingRequests as SlowOutgoingRequestsRecorder; use Livewire\Attributes\Lazy; use Livewire\Attributes\Url; @@ -16,8 +15,6 @@ #[Lazy] class SlowOutgoingRequests extends Card { - use Concerns\HasPeriod, Concerns\RemembersQueries; - /** * Ordering. * @@ -32,10 +29,9 @@ class SlowOutgoingRequests extends Card public function render(): Renderable { [$slowOutgoingRequests, $time, $runAt] = $this->remember( - fn () => Pulse::aggregate( + fn () => $this->aggregate( 'slow_outgoing_request', ['max', 'count'], - $this->periodAsInterval(), match ($this->orderBy) { 'count' => 'count', default => 'max', diff --git a/src/Livewire/SlowQueries.php b/src/Livewire/SlowQueries.php index 78df9a1b..51029551 100644 --- a/src/Livewire/SlowQueries.php +++ b/src/Livewire/SlowQueries.php @@ -5,7 +5,6 @@ use Illuminate\Contracts\Support\Renderable; use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\View; -use Laravel\Pulse\Facades\Pulse; use Laravel\Pulse\Recorders\SlowQueries as SlowQueriesRecorder; use Livewire\Attributes\Lazy; use Livewire\Attributes\Url; @@ -16,8 +15,6 @@ #[Lazy] class SlowQueries extends Card { - use Concerns\HasPeriod, Concerns\RemembersQueries; - /** * Ordering. * @@ -32,10 +29,9 @@ class SlowQueries extends Card public function render(): Renderable { [$slowQueries, $time, $runAt] = $this->remember( - fn () => Pulse::aggregate( + fn () => $this->aggregate( 'slow_query', ['max', 'count'], - $this->periodAsInterval(), match ($this->orderBy) { 'count' => 'count', default => 'max', diff --git a/src/Livewire/SlowRequests.php b/src/Livewire/SlowRequests.php index 2a1abaae..159e8813 100644 --- a/src/Livewire/SlowRequests.php +++ b/src/Livewire/SlowRequests.php @@ -5,7 +5,6 @@ use Illuminate\Contracts\Support\Renderable; use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\View; -use Laravel\Pulse\Facades\Pulse; use Laravel\Pulse\Recorders\SlowRequests as SlowRequestsRecorder; use Livewire\Attributes\Lazy; use Livewire\Attributes\Url; @@ -16,8 +15,6 @@ #[Lazy] class SlowRequests extends Card { - use Concerns\HasPeriod, Concerns\RemembersQueries; - /** * Ordering. * @@ -32,10 +29,9 @@ class SlowRequests extends Card public function render(): Renderable { [$slowRequests, $time, $runAt] = $this->remember( - fn () => Pulse::aggregate( + fn () => $this->aggregate( 'slow_request', ['max', 'count'], - $this->periodAsInterval(), match ($this->orderBy) { 'count' => 'count', default => 'max', diff --git a/src/Livewire/Usage.php b/src/Livewire/Usage.php index 5e30065e..9a99a4ad 100644 --- a/src/Livewire/Usage.php +++ b/src/Livewire/Usage.php @@ -18,8 +18,6 @@ #[Lazy] class Usage extends Card { - use Concerns\HasPeriod, Concerns\RemembersQueries; - /** * The type of usage to show. * @@ -44,14 +42,13 @@ public function render(): Renderable [$userRequestCounts, $time, $runAt] = $this->remember( function () use ($type) { - $counts = Pulse::aggregate( + $counts = $this->aggregate( match ($type) { 'requests' => 'user_request', 'slow_requests' => 'slow_user_request', 'jobs' => 'user_job', }, 'count', - $this->periodAsInterval(), limit: 10, ); diff --git a/src/Storage/DatabaseStorage.php b/src/Storage/DatabaseStorage.php index 0ae497aa..15942bea 100644 --- a/src/Storage/DatabaseStorage.php +++ b/src/Storage/DatabaseStorage.php @@ -394,23 +394,17 @@ protected function periods(): array * Retrieve values for the given type. * * @param list $keys - * @return \Illuminate\Support\Collection< - * int, - * array< - * string, - * array{ - * timestamp: int, - * type: string, - * key: string, - * value: string - * } - * > - * > + * @return \Illuminate\Support\Collection */ public function values(string $type, ?array $keys = null): Collection { return $this->connection() ->table('pulse_values') + ->select('timestamp', 'key', 'value') ->where('type', $type) ->when($keys, fn ($query) => $query->whereIn('key', $keys)) ->get() From ab51a862cee1349fd1c624b12af7524b3066aebc Mon Sep 17 00:00:00 2001 From: James Brooks Date: Wed, 13 Dec 2023 15:00:38 +0000 Subject: [PATCH 076/110] [1.x] Add CacheInteractions ignore key for Vapor job attempts (#221) * Add CacheInteractions ignore key for Vapor job attempts * internalize keys * formatting --------- Co-authored-by: Taylor Otwell --- config/pulse.php | 8 ++------ src/Pulse.php | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/config/pulse.php b/config/pulse.php index a52f5c31..81675ae1 100644 --- a/config/pulse.php +++ b/config/pulse.php @@ -1,6 +1,7 @@ env('PULSE_CACHE_INTERACTIONS_ENABLED', true), 'sample_rate' => env('PULSE_CACHE_INTERACTIONS_SAMPLE_RATE', 1), 'ignore' => [ - '/^laravel:pulse:/', // Internal Pulse keys... - '/^illuminate:/', // Internal Laravel keys... - '/^telescope:/', // Internal Telescope keys... - '/^nova/', // Internal Nova keys... - '/^.+@.+\|(?:(?:\d+\.\d+\.\d+\.\d+)|[0-9a-fA-F:]+)(?::timer)?$/', // Breeze / Jetstream authentication rate limiting... - '/^[a-zA-Z0-9]{40}$/', // Session IDs... + ...Pulse::vendorCacheKeys(), ], 'groups' => [ '/^job-exceptions:.*/' => 'job-exceptions:*', diff --git a/src/Pulse.php b/src/Pulse.php index f49aa130..0bf0440f 100644 --- a/src/Pulse.php +++ b/src/Pulse.php @@ -491,6 +491,22 @@ public function js(): string return "".PHP_EOL; } + /** + * The default "vendor" cache keys that should be ignored by Pulse. + */ + public static function vendorCacheKeys(): array + { + return [ + '/(^laravel_vapor_job_attemp(t?)s:)/', // Laravel Vapor keys... + '/^.+@.+\|(?:(?:\d+\.\d+\.\d+\.\d+)|[0-9a-fA-F:]+)(?::timer)?$/', // Breeze / Jetstream keys... + '/^[a-zA-Z0-9]{40}$/', // Session IDs... + '/^illuminate:/', // Laravel keys... + '/^laravel:pulse:/', // Pulse keys... + '/^nova/', // Nova keys... + '/^telescope:/', // Telescope keys... + ]; + } + /** * Determine if Pulse may register routes. */ From 88b2444dc3a5a25c7a16dd759373a3daa5fbb724 Mon Sep 17 00:00:00 2001 From: taylorotwell Date: Wed, 13 Dec 2023 15:01:04 +0000 Subject: [PATCH 077/110] Update facade docblocks --- src/Facades/Pulse.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Facades/Pulse.php b/src/Facades/Pulse.php index 5fd78168..6359a736 100644 --- a/src/Facades/Pulse.php +++ b/src/Facades/Pulse.php @@ -26,6 +26,7 @@ * @method static \Laravel\Pulse\Pulse rememberUser(\Illuminate\Contracts\Auth\Authenticatable $user) * @method static \Laravel\Pulse\Pulse|string css(string|\Illuminate\Contracts\Support\Htmlable|array|null $css = null) * @method static string js() + * @method static array vendorCacheKeys() * @method static bool registersRoutes() * @method static \Laravel\Pulse\Pulse ignoreRoutes() * @method static \Laravel\Pulse\Pulse handleExceptionsUsing(callable $callback) From 98343e75bd366cd42d2c567a89da8211434f5d18 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Wed, 13 Dec 2023 10:47:48 -0600 Subject: [PATCH 078/110] update method name --- config/pulse.php | 2 +- src/Pulse.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/pulse.php b/config/pulse.php index 81675ae1..28543ed9 100644 --- a/config/pulse.php +++ b/config/pulse.php @@ -135,7 +135,7 @@ 'enabled' => env('PULSE_CACHE_INTERACTIONS_ENABLED', true), 'sample_rate' => env('PULSE_CACHE_INTERACTIONS_SAMPLE_RATE', 1), 'ignore' => [ - ...Pulse::vendorCacheKeys(), + ...Pulse::defaultVendorCacheKeys(), ], 'groups' => [ '/^job-exceptions:.*/' => 'job-exceptions:*', diff --git a/src/Pulse.php b/src/Pulse.php index 0bf0440f..3916f2e5 100644 --- a/src/Pulse.php +++ b/src/Pulse.php @@ -494,7 +494,7 @@ public function js(): string /** * The default "vendor" cache keys that should be ignored by Pulse. */ - public static function vendorCacheKeys(): array + public static function defaultVendorCacheKeys(): array { return [ '/(^laravel_vapor_job_attemp(t?)s:)/', // Laravel Vapor keys... From 9eda727da25be60c838fb604daced2073c1607fd Mon Sep 17 00:00:00 2001 From: taylorotwell Date: Wed, 13 Dec 2023 16:48:37 +0000 Subject: [PATCH 079/110] Update facade docblocks --- src/Facades/Pulse.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Facades/Pulse.php b/src/Facades/Pulse.php index 6359a736..896bba82 100644 --- a/src/Facades/Pulse.php +++ b/src/Facades/Pulse.php @@ -26,7 +26,7 @@ * @method static \Laravel\Pulse\Pulse rememberUser(\Illuminate\Contracts\Auth\Authenticatable $user) * @method static \Laravel\Pulse\Pulse|string css(string|\Illuminate\Contracts\Support\Htmlable|array|null $css = null) * @method static string js() - * @method static array vendorCacheKeys() + * @method static array defaultVendorCacheKeys() * @method static bool registersRoutes() * @method static \Laravel\Pulse\Pulse ignoreRoutes() * @method static \Laravel\Pulse\Pulse handleExceptionsUsing(callable $callback) From 3a699dfa94a1f01bfd5fecea5c06725d496d65c0 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 14 Dec 2023 15:12:43 +0800 Subject: [PATCH 080/110] Fixes PHPStan error (#226) --- src/Pulse.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Pulse.php b/src/Pulse.php index 3916f2e5..f822bfd6 100644 --- a/src/Pulse.php +++ b/src/Pulse.php @@ -493,6 +493,8 @@ public function js(): string /** * The default "vendor" cache keys that should be ignored by Pulse. + * + * @return list */ public static function defaultVendorCacheKeys(): array { From 12c673931f25d88ffdc5d2fd4f5b0c469b9a632f Mon Sep 17 00:00:00 2001 From: Vinay <45792827+Vinya007@users.noreply.github.com> Date: Thu, 14 Dec 2023 20:29:47 +0530 Subject: [PATCH 081/110] Added missing favicon (#228) * Added missing favicon * Add base64 encode favicon --- resources/views/components/pulse.blade.php | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/views/components/pulse.blade.php b/resources/views/components/pulse.blade.php index 4a2c3458..64e37298 100644 --- a/resources/views/components/pulse.blade.php +++ b/resources/views/components/pulse.blade.php @@ -10,6 +10,7 @@ + {!! Laravel\Pulse\Facades\Pulse::css() !!} From 6b9fcad3526c977a75e391a0c0ab41e201826c66 Mon Sep 17 00:00:00 2001 From: Robert Fridzema Date: Thu, 14 Dec 2023 16:44:53 +0100 Subject: [PATCH 082/110] Renamed purge to clear (#230) * Renamed purge to clear * Fix tests * Update ClearCommand.php * add alias --------- Co-authored-by: Taylor Otwell --- .../{PurgeCommand.php => ClearCommand.php} | 15 +++++++++++---- src/PulseServiceProvider.php | 2 +- ...{PurgeCommandTest.php => ClearCommandTest.php} | 6 +++--- 3 files changed, 15 insertions(+), 8 deletions(-) rename src/Commands/{PurgeCommand.php => ClearCommand.php} (78%) rename tests/Feature/Commands/{PurgeCommandTest.php => ClearCommandTest.php} (92%) diff --git a/src/Commands/PurgeCommand.php b/src/Commands/ClearCommand.php similarity index 78% rename from src/Commands/PurgeCommand.php rename to src/Commands/ClearCommand.php index 87eadbed..c216e53c 100644 --- a/src/Commands/PurgeCommand.php +++ b/src/Commands/ClearCommand.php @@ -10,8 +10,8 @@ /** * @internal */ -#[AsCommand(name: 'pulse:purge')] -class PurgeCommand extends Command +#[AsCommand(name: 'pulse:clear')] +class ClearCommand extends Command { use ConfirmableTrait; @@ -20,7 +20,7 @@ class PurgeCommand extends Command * * @var string */ - public $signature = 'pulse:purge {--type=* : Only clear the specified type(s)} + public $signature = 'pulse:clear {--type=* : Only clear the specified type(s)} {--force : Force the operation to run when in production}'; /** @@ -28,7 +28,14 @@ class PurgeCommand extends Command * * @var string */ - public $description = 'Purge Pulse data'; + public $description = 'Delete all Pulse data from storage'; + + /** + * The console command name aliases. + * + * @var array + */ + protected $aliases = ['pulse:purge']; /** * Handle the command. diff --git a/src/PulseServiceProvider.php b/src/PulseServiceProvider.php index b93131b1..cf344b70 100644 --- a/src/PulseServiceProvider.php +++ b/src/PulseServiceProvider.php @@ -214,7 +214,7 @@ protected function registerCommands(): void Commands\WorkCommand::class, Commands\CheckCommand::class, Commands\RestartCommand::class, - Commands\PurgeCommand::class, + Commands\ClearCommand::class, ]); } } diff --git a/tests/Feature/Commands/PurgeCommandTest.php b/tests/Feature/Commands/ClearCommandTest.php similarity index 92% rename from tests/Feature/Commands/PurgeCommandTest.php rename to tests/Feature/Commands/ClearCommandTest.php index e0501285..8ffcde4d 100644 --- a/tests/Feature/Commands/PurgeCommandTest.php +++ b/tests/Feature/Commands/ClearCommandTest.php @@ -4,7 +4,7 @@ use Illuminate\Support\Facades\DB; use Laravel\Pulse\Facades\Pulse; -it('purges Pulse data', function () { +it('clears Pulse data', function () { Pulse::set('foo', 'bar', 'baz'); Pulse::record('foo', 'bar', 123)->max()->count(); Pulse::store(); @@ -13,7 +13,7 @@ expect(DB::table('pulse_entries')->count())->toBe(1); expect(DB::table('pulse_aggregates')->count())->toBe(8); - Artisan::call('pulse:purge'); + Artisan::call('pulse:clear'); expect(DB::table('pulse_values')->count())->toBe(0); expect(DB::table('pulse_entries')->count())->toBe(0); @@ -31,7 +31,7 @@ expect(DB::table('pulse_entries')->count())->toBe(2); expect(DB::table('pulse_aggregates')->count())->toBe(16); - Artisan::call('pulse:purge --type delete-me'); + Artisan::call('pulse:clear --type delete-me'); expect(DB::table('pulse_values')->where('type', 'keep-me')->count())->toBe(1); expect(DB::table('pulse_values')->where('type', 'delete-me')->count())->toBe(0); From 4d3b6f766c579ee56b8da10f3402157d71cbf7a3 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Thu, 14 Dec 2023 10:09:07 -0600 Subject: [PATCH 083/110] update migration --- .../2023_06_07_000001_create_pulse_tables.php | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) 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 156ff225..c6e87554 100644 --- a/database/migrations/2023_06_07_000001_create_pulse_tables.php +++ b/database/migrations/2023_06_07_000001_create_pulse_tables.php @@ -8,19 +8,15 @@ return new class extends Migration { - /** - * Get the migration connection name. - */ - public function getConnection(): ?string - { - return Config::get('pulse.storage.database.connection'); - } - /** * Run the migrations. */ public function up(): void { + if (! $this->shouldRun()) { + return; + } + $connection = DB::connection($this->getConnection()); Schema::create('pulse_values', function (Blueprint $table) use ($connection) { @@ -89,4 +85,20 @@ public function down(): void Schema::dropIfExists('pulse_entries'); Schema::dropIfExists('pulse_aggregates'); } + + /** + * Get the migration connection name. + */ + public function getConnection(): ?string + { + return Config::get('pulse.storage.database.connection'); + } + + /** + * Determine if the migration should run. + */ + public function shouldRun(): bool + { + return ! App::environment('testing') || config('pulse.enabled'); + } }; From 05ffea026370ef8b7bd573feb7653ef4ba9e685a Mon Sep 17 00:00:00 2001 From: Francisco Madeira Date: Thu, 14 Dec 2023 19:34:42 +0000 Subject: [PATCH 084/110] feat: Add scrollbars styling. (#232) --- resources/views/components/scroll.blade.php | 2 +- tailwind.config.js | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/resources/views/components/scroll.blade.php b/resources/views/components/scroll.blade.php index ca1413a4..ee9d54e2 100644 --- a/resources/views/components/scroll.blade.php +++ b/resources/views/components/scroll.blade.php @@ -22,7 +22,7 @@ }" {{ $attributes->merge(['class' => '@container/scroll-wrapper flex-grow flex w-full overflow-hidden' . ($expand ? '' : ' basis-56'), ':class' => "loading && 'opacity-25 animate-pulse'"]) }} > -
+
{{ $slot }}
diff --git a/tailwind.config.js b/tailwind.config.js index efb6361e..ccc9a92f 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -33,6 +33,10 @@ module.exports = { require("@tailwindcss/container-queries"), function ({ addVariant }) { addVariant('default', 'html :where(&)') - } + addVariant('supports-scrollbars', '@supports selector(::-webkit-scrollbar)'), + addVariant('scrollbar', '&::-webkit-scrollbar') + addVariant('scrollbar-track', '&::-webkit-scrollbar-track') + addVariant('scrollbar-thumb', '&::-webkit-scrollbar-thumb') + }, ], }; From 0d91d940e5923c54cfd6bf20dcb92bfcfd62d334 Mon Sep 17 00:00:00 2001 From: Xu Chunyang Date: Fri, 15 Dec 2023 03:38:44 +0800 Subject: [PATCH 085/110] [1.x] Show pulse version from artisan about command (#156) * Show pulse version from artisan about command * Ensure is only run in the console * Display whether Pulse is enabled * Format enabled output * Bump minimum Laravel dependencies --------- Co-authored-by: Tim MacDonald Co-authored-by: James Brooks --- composer.json | 26 +++++++++++++------------- src/PulseServiceProvider.php | 7 +++++++ 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/composer.json b/composer.json index 75436910..8f7e96c2 100644 --- a/composer.json +++ b/composer.json @@ -18,19 +18,19 @@ "php": "^8.1", "doctrine/sql-formatter": "^1.1", "guzzlehttp/promises": "^1.0 || ^2.0", - "illuminate/auth": "^10.21", - "illuminate/cache": "^10.21", - "illuminate/config": "^10.21", - "illuminate/console": "^10.21", - "illuminate/contracts": "^10.21", - "illuminate/database": "^10.21", - "illuminate/events": "^10.21", - "illuminate/http": "^10.21", - "illuminate/queue": "^10.21", - "illuminate/redis": "^10.21", - "illuminate/routing": "^10.21", - "illuminate/support": "^10.21", - "illuminate/view": "^10.21", + "illuminate/auth": "^10.34", + "illuminate/cache": "^10.34", + "illuminate/config": "^10.34", + "illuminate/console": "^10.34", + "illuminate/contracts": "^10.34", + "illuminate/database": "^10.34", + "illuminate/events": "^10.34", + "illuminate/http": "^10.34", + "illuminate/queue": "^10.34", + "illuminate/redis": "^10.34", + "illuminate/routing": "^10.34", + "illuminate/support": "^10.34", + "illuminate/view": "^10.34", "livewire/livewire": "^3.2", "nesbot/carbon": "^2.67" }, diff --git a/src/PulseServiceProvider.php b/src/PulseServiceProvider.php index cf344b70..b21d1802 100644 --- a/src/PulseServiceProvider.php +++ b/src/PulseServiceProvider.php @@ -2,12 +2,14 @@ namespace Laravel\Pulse; +use Composer\InstalledVersions; use Illuminate\Auth\Events\Logout; use Illuminate\Contracts\Auth\Access\Gate; use Illuminate\Contracts\Console\Kernel as ConsoleKernel; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Contracts\Foundation\Application; use Illuminate\Contracts\Http\Kernel as HttpKernel; +use Illuminate\Foundation\Console\AboutCommand; use Illuminate\Queue\Events\Looping; use Illuminate\Queue\Events\WorkerStopping; use Illuminate\Routing\Router; @@ -216,6 +218,11 @@ protected function registerCommands(): void Commands\RestartCommand::class, Commands\ClearCommand::class, ]); + + AboutCommand::add('Pulse', fn () => [ + 'Version' => InstalledVersions::getPrettyVersion('laravel/pulse'), + 'Enabled' => AboutCommand::format(config('pulse.enabled'), console: fn ($value) => $value ? 'ENABLED' : 'OFF'), + ]); } } } From 7cf1593ef6b26df0745b85e7e05229ee7b61519c Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Fri, 15 Dec 2023 10:30:59 +1100 Subject: [PATCH 086/110] [1.x] Improve migration errors to guide developers with unsupported drivers (#233) * Improve migration errors to guide developers with unsupported drivers * Fix code styling * Fix * Update PulseMigration.php --------- Co-authored-by: timacdonald Co-authored-by: Taylor Otwell --- .../2023_06_07_000001_create_pulse_tables.php | 39 +++------------ src/Support/PulseMigration.php | 49 +++++++++++++++++++ 2 files changed, 57 insertions(+), 31 deletions(-) create mode 100644 src/Support/PulseMigration.php 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 c6e87554..38c7fdde 100644 --- a/database/migrations/2023_06_07_000001_create_pulse_tables.php +++ b/database/migrations/2023_06_07_000001_create_pulse_tables.php @@ -1,12 +1,10 @@ getConnection()); - - Schema::create('pulse_values', function (Blueprint $table) use ($connection) { + Schema::create('pulse_values', function (Blueprint $table) { $table->id(); $table->unsignedInteger('timestamp'); $table->string('type'); $table->mediumText('key'); - match ($driver = $connection->getDriverName()) { + match ($this->driver()) { 'mysql' => $table->char('key_hash', 16)->charset('binary')->virtualAs('unhex(md5(`key`))'), 'pgsql' => $table->uuid('key_hash')->storedAs('md5("key")::uuid'), - default => throw new RuntimeException("Unsupported database driver [{$driver}]."), }; $table->mediumText('value'); @@ -36,15 +31,14 @@ public function up(): void $table->unique(['type', 'key_hash']); // For data integrity and upserts... }); - Schema::create('pulse_entries', function (Blueprint $table) use ($connection) { + Schema::create('pulse_entries', function (Blueprint $table) { $table->id(); $table->unsignedInteger('timestamp'); $table->string('type'); $table->mediumText('key'); - match ($driver = $connection->getDriverName()) { + match ($this->driver()) { 'mysql' => $table->char('key_hash', 16)->charset('binary')->virtualAs('unhex(md5(`key`))'), 'pgsql' => $table->uuid('key_hash')->storedAs('md5("key")::uuid'), - default => throw new RuntimeException("Unsupported database driver [{$driver}]."), }; $table->bigInteger('value')->nullable(); @@ -54,16 +48,15 @@ public function up(): void $table->index(['timestamp', 'type', 'key_hash', 'value']); // For aggregate queries... }); - Schema::create('pulse_aggregates', function (Blueprint $table) use ($connection) { + Schema::create('pulse_aggregates', function (Blueprint $table) { $table->id(); $table->unsignedInteger('bucket'); $table->unsignedMediumInteger('period'); $table->string('type'); $table->mediumText('key'); - match ($driver = $connection->getDriverName()) { + match ($this->driver()) { 'mysql' => $table->char('key_hash', 16)->charset('binary')->virtualAs('unhex(md5(`key`))'), 'pgsql' => $table->uuid('key_hash')->storedAs('md5("key")::uuid'), - default => throw new RuntimeException("Unsupported database driver [{$driver}]."), }; $table->string('aggregate'); $table->decimal('value', 20, 2); @@ -85,20 +78,4 @@ public function down(): void Schema::dropIfExists('pulse_entries'); Schema::dropIfExists('pulse_aggregates'); } - - /** - * Get the migration connection name. - */ - public function getConnection(): ?string - { - return Config::get('pulse.storage.database.connection'); - } - - /** - * Determine if the migration should run. - */ - public function shouldRun(): bool - { - return ! App::environment('testing') || config('pulse.enabled'); - } }; diff --git a/src/Support/PulseMigration.php b/src/Support/PulseMigration.php new file mode 100644 index 00000000..ceb75211 --- /dev/null +++ b/src/Support/PulseMigration.php @@ -0,0 +1,49 @@ +driver(), ['mysql', 'pgsql'])) { + return true; + } + + if (! App::environment('testing')) { + throw new RuntimeException("Pulse does not support the [{$this->driver()}] database driver."); + } + + if (Config::get('pulse.enabled')) { + throw new RuntimeException("Pulse does not support the [{$this->driver()}] database driver. You can disable Pulse in your testsuite by adding `` to your project's `phpunit.xml` file."); + } + + return false; + } + + /** + * Get the database connection driver. + */ + protected function driver(): string + { + return DB::connection($this->getConnection())->getDriverName(); + } +} From ccec80e03341a138e0d604bf97a967ab7cfaceb7 Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Fri, 15 Dec 2023 10:41:07 +1100 Subject: [PATCH 087/110] Fix types --- src/Commands/ClearCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Commands/ClearCommand.php b/src/Commands/ClearCommand.php index c216e53c..f4b4d8a0 100644 --- a/src/Commands/ClearCommand.php +++ b/src/Commands/ClearCommand.php @@ -33,7 +33,7 @@ class ClearCommand extends Command /** * The console command name aliases. * - * @var array + * @var array */ protected $aliases = ['pulse:purge']; From 5cf22c4aba79520484e3aa3b07a7d07435b0574c Mon Sep 17 00:00:00 2001 From: Jess Archer Date: Fri, 15 Dec 2023 23:53:36 +1000 Subject: [PATCH 088/110] [1.x] Support BusyBox/Alpine `top` output (#236) * Support BusyBox/Alpine `top` output * Fix test in CI --- src/Recorders/Servers.php | 2 +- tests/Feature/Recorders/ServersTest.php | 35 +++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 tests/Feature/Recorders/ServersTest.php diff --git a/src/Recorders/Servers.php b/src/Recorders/Servers.php index 84557f82..1101d85d 100644 --- a/src/Recorders/Servers.php +++ b/src/Recorders/Servers.php @@ -58,7 +58,7 @@ public function record(SharedBeat $event): void $cpu = match (PHP_OS_FAMILY) { 'Darwin' => (int) `top -l 1 | grep -E "^CPU" | tail -1 | awk '{ print $3 + $5 }'`, - 'Linux' => (int) `top -bn1 | grep '%Cpu(s)' | tail -1 | grep -Eo '[0-9]+\.[0-9]+' | head -n 4 | tail -1 | awk '{ print 100 - $1 }'`, + 'Linux' => (int) `top -bn1 | grep -E '^(%Cpu|CPU)' | awk '{ print $2 + $4 }'`, 'Windows' => (int) trim(`wmic cpu get loadpercentage | more +1`), default => throw new RuntimeException('The pulse:check command does not currently support '.PHP_OS_FAMILY), }; diff --git a/tests/Feature/Recorders/ServersTest.php b/tests/Feature/Recorders/ServersTest.php new file mode 100644 index 00000000..5ef4874c --- /dev/null +++ b/tests/Feature/Recorders/ServersTest.php @@ -0,0 +1,35 @@ +startOfMinute()); + event(app(SharedBeat::class)); + Pulse::store(); + + expect(Pulse::ignore(fn () => DB::table('pulse_entries')->count()))->toBe(0); + + $value = Pulse::ignore(fn () => DB::table('pulse_values')->sole()); + expect($value->type)->toBe('system'); + expect($value->key)->toBe('foo'); + expect($value->timestamp)->toBe(Date::now()->startOfMinute()->timestamp); + $payload = json_decode($value->value); + expect($payload->name)->toBe('Foo'); + expect($payload->cpu)->toBeGreaterThanOrEqual(0); + expect($payload->cpu)->toBeLessThanOrEqual(100); + expect($payload->memory_used)->toBeGreaterThan(0); + expect($payload->memory_total)->toBeGreaterThan(0); + + $aggregates = Pulse::ignore(fn () => DB::table('pulse_aggregates')->get()); + expect($aggregates->count())->toBe(8); + expect($aggregates->pluck('type')->unique()->values()->all())->toBe(['cpu', 'memory']); + expect($aggregates->pluck('period')->unique()->values()->all())->toBe([60, 360, 1440, 10080]); + expect($aggregates->pluck('key')->unique()->values()->all())->toBe(['foo']); + expect($aggregates->pluck('aggregate')->unique()->values()->all())->toBe(['avg']); +}); From 56323e1f60f9a8ed35de40bcba3243c794082b64 Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Sat, 16 Dec 2023 00:54:13 +1100 Subject: [PATCH 089/110] [1.x] Ignore all internal pulse activity (#234) * Ignore interal pulse activity * Go all in on digest * Ignore filtering * Improve ignoring * remove temp var * Update facade --- src/Commands/CheckCommand.php | 2 +- src/Commands/ClearCommand.php | 8 +- src/Commands/WorkCommand.php | 10 +-- src/Contracts/Ingest.php | 8 +- src/Facades/Pulse.php | 3 +- src/Ingests/RedisIngest.php | 4 +- src/Ingests/StorageIngest.php | 4 +- src/Pulse.php | 66 ++++++++------ src/PulseServiceProvider.php | 6 +- tests/Feature/Commands/ClearCommandTest.php | 42 +++++---- tests/Feature/Ingests/DatabaseTest.php | 4 + tests/Feature/Livewire/CacheTest.php | 6 +- tests/Feature/Livewire/ExceptionsTest.php | 2 +- tests/Feature/Livewire/QueuesTest.php | 2 +- tests/Feature/Livewire/ServersTest.php | 2 +- tests/Feature/Livewire/SlowJobsTest.php | 2 +- .../Livewire/SlowOutgoingRequestsTest.php | 2 +- tests/Feature/Livewire/SlowQueriesTest.php | 2 +- tests/Feature/Livewire/SlowRequestsTest.php | 2 +- tests/Feature/Livewire/UsageTest.php | 2 +- tests/Feature/PulseTest.php | 6 +- .../Recorders/CacheInteractionsTest.php | 22 ++--- tests/Feature/Recorders/ExceptionsTest.php | 14 +-- tests/Feature/Recorders/QueuesTest.php | 87 ++++++++++--------- tests/Feature/Recorders/SlowJobsTest.php | 36 ++++---- .../Recorders/SlowOutgoingRequestsTest.php | 22 ++--- tests/Feature/Recorders/SlowQueriesTest.php | 19 ++-- tests/Feature/RedisTest.php | 2 +- tests/Feature/Storage/DatabaseStorageTest.php | 28 +++--- tests/Pest.php | 6 +- 30 files changed, 227 insertions(+), 194 deletions(-) diff --git a/src/Commands/CheckCommand.php b/src/Commands/CheckCommand.php index 8cdf3604..6905a28b 100644 --- a/src/Commands/CheckCommand.php +++ b/src/Commands/CheckCommand.php @@ -72,7 +72,7 @@ public function handle( $event->dispatch(new IsolatedBeat($lastSnapshotAt, $interval)); } - $pulse->store(); + $pulse->ingest(); } } } diff --git a/src/Commands/ClearCommand.php b/src/Commands/ClearCommand.php index f4b4d8a0..411846fd 100644 --- a/src/Commands/ClearCommand.php +++ b/src/Commands/ClearCommand.php @@ -4,7 +4,7 @@ use Illuminate\Console\Command; use Illuminate\Console\ConfirmableTrait; -use Laravel\Pulse\Contracts\Storage; +use Laravel\Pulse\Pulse; use Symfony\Component\Console\Attribute\AsCommand; /** @@ -40,7 +40,7 @@ class ClearCommand extends Command /** * Handle the command. */ - public function handle(Storage $storage): int + public function handle(Pulse $pulse): int { if (! $this->confirmToProceed()) { return Command::FAILURE; @@ -49,12 +49,12 @@ public function handle(Storage $storage): int if (is_array($this->option('type')) && count($this->option('type')) > 0) { $this->components->task( 'Purging Pulse data for ['.implode(', ', $this->option('type')).']', - fn () => $storage->purge($this->option('type')) + fn () => $pulse->purge($this->option('type')) ); } else { $this->components->task( 'Purging all Pulse data', - fn () => $storage->purge() + fn () => $pulse->purge(), ); } diff --git a/src/Commands/WorkCommand.php b/src/Commands/WorkCommand.php index e1728f45..c59b9602 100644 --- a/src/Commands/WorkCommand.php +++ b/src/Commands/WorkCommand.php @@ -5,8 +5,7 @@ use Carbon\CarbonImmutable; use Illuminate\Console\Command; use Illuminate\Support\Sleep; -use Laravel\Pulse\Contracts\Ingest; -use Laravel\Pulse\Contracts\Storage; +use Laravel\Pulse\Pulse; use Laravel\Pulse\Support\CacheStoreResolver; use Symfony\Component\Console\Attribute\AsCommand; @@ -34,8 +33,7 @@ class WorkCommand extends Command * Handle the command. */ public function handle( - Ingest $ingest, - Storage $storage, + Pulse $pulse, CacheStoreResolver $cache, ): int { $lastRestart = $cache->store()->get('laravel:pulse:restart'); @@ -49,10 +47,10 @@ public function handle( return self::SUCCESS; } - $ingest->store($storage); + $pulse->digest(); if ($now->subMinutes(10)->greaterThan($lastTrimmedStorageAt)) { - $storage->trim(); + $pulse->trim(); $lastTrimmedStorageAt = $now; } diff --git a/src/Contracts/Ingest.php b/src/Contracts/Ingest.php index 51a72ff2..fcee098a 100644 --- a/src/Contracts/Ingest.php +++ b/src/Contracts/Ingest.php @@ -14,12 +14,12 @@ interface Ingest public function ingest(Collection $items): void; /** - * Trim the ingest. + * Digest the ingested items. */ - public function trim(): void; + public function digest(Storage $storage): int; /** - * Store the ingested items. + * Trim the ingest. */ - public function store(Storage $storage): int; + public function trim(): void; } diff --git a/src/Facades/Pulse.php b/src/Facades/Pulse.php index 896bba82..8dc9ca2e 100644 --- a/src/Facades/Pulse.php +++ b/src/Facades/Pulse.php @@ -15,7 +15,8 @@ * @method static mixed ignore(callable $callback) * @method static \Laravel\Pulse\Pulse flush() * @method static \Laravel\Pulse\Pulse filter(callable $filter) - * @method static int store() + * @method static int ingest() + * @method static int digest() * @method static \Illuminate\Support\Collection recorders() * @method static \Illuminate\Support\Collection resolveUsers(\Illuminate\Support\Collection $ids) * @method static \Laravel\Pulse\Pulse users(callable $callback) diff --git a/src/Ingests/RedisIngest.php b/src/Ingests/RedisIngest.php index 809468e1..43b06ac9 100644 --- a/src/Ingests/RedisIngest.php +++ b/src/Ingests/RedisIngest.php @@ -71,9 +71,9 @@ public function trim(): void } /** - * Store the ingested items. + * Digest the ingested items. */ - public function store(Storage $storage): int + public function digest(Storage $storage): int { $total = 0; diff --git a/src/Ingests/StorageIngest.php b/src/Ingests/StorageIngest.php index 16d5fda4..891223a6 100644 --- a/src/Ingests/StorageIngest.php +++ b/src/Ingests/StorageIngest.php @@ -38,9 +38,9 @@ public function trim(): void } /** - * Store the ingested items. + * Digest the ingested items. */ - public function store(Storage $storage): int + public function digest(Storage $storage): int { return 0; } diff --git a/src/Pulse.php b/src/Pulse.php index f822bfd6..168869a0 100644 --- a/src/Pulse.php +++ b/src/Pulse.php @@ -286,40 +286,58 @@ public function filter(callable $filter): self } /** - * Store the queued items. + * Ingest the entries. */ - public function store(): int + public function ingest(): int { - $entries = $this->rescue(function () { - $this->lazy->each(fn ($lazy) => $lazy()); + $this->rescue(fn () => $this->lazy->each(fn ($lazy) => $lazy())); - return $this->entries->filter($this->shouldRecord(...)); - }) ?? collect([]); + return $this->ignore(function () { + $entries = $this->rescue(fn () => $this->entries->filter($this->shouldRecord(...))) ?? collect([]); - if ($entries->isEmpty()) { - $this->flush(); + if ($entries->isEmpty()) { + $this->flush(); - return 0; - } + return 0; + } + + $ingest = $this->app->make(Ingest::class); - $ingest = $this->app->make(Ingest::class); + $count = $this->rescue(function () use ($entries, $ingest) { + $ingest->ingest($entries); - $count = $this->rescue(function () use ($entries, $ingest) { - $ingest->ingest($entries); + return $entries->count(); + }) ?? 0; - return $entries->count(); - }) ?? 0; + // TODO remove fallback when tagging v1 + $odds = $this->app->make('config')->get('pulse.ingest.trim.lottery') ?? $this->app->make('config')->get('pulse.ingest.trim_lottery'); - // TODO remove fallback when tagging v1 - $odds = $this->app->make('config')->get('pulse.ingest.trim.lottery') ?? $this->app->make('config')->get('pulse.ingest.trim_lottery'); + Lottery::odds(...$odds) + ->winner(fn () => $this->rescue(fn () => $ingest->trim(...))) + ->choose(); - Lottery::odds(...$odds) - ->winner(fn () => $this->rescue($ingest->trim(...))) - ->choose(); + $this->flush(); + + return $count; + }); + } - $this->flush(); + /** + * Digest the entries. + */ + public function digest(): int + { + return $this->ignore( + fn () => $this->app->make(Ingest::class)->digest($this->app->make(Storage::class)) + ); + } - return $count; + /** + * Determine if Pulse wants to ingest entries. + */ + public function wantsIngesting(): bool + { + return $this->lazy->isNotEmpty() || $this->entries->isNotEmpty(); } /** @@ -579,8 +597,6 @@ public function setContainer($container) */ public function __call($method, $parameters): mixed { - $storage = $this->app->make(Storage::class); - - return $this->forwardCallTo($storage, $method, $parameters); + return $this->ignore(fn () => $this->forwardCallTo($this->app->make(Storage::class), $method, $parameters)); } } diff --git a/src/PulseServiceProvider.php b/src/PulseServiceProvider.php index b21d1802..eb27b032 100644 --- a/src/PulseServiceProvider.php +++ b/src/PulseServiceProvider.php @@ -123,19 +123,19 @@ protected function listenForEvents(): void Looping::class, WorkerStopping::class, ], function () use ($app) { - $app->make(Pulse::class)->store(); + $app->make(Pulse::class)->ingest(); }); }); $this->callAfterResolving(HttpKernel::class, function (HttpKernel $kernel, Application $app) { $kernel->whenRequestLifecycleIsLongerThan(-1, function () use ($app) { // @phpstan-ignore method.notFound - $app->make(Pulse::class)->store(); + $app->make(Pulse::class)->ingest(); }); }); $this->callAfterResolving(ConsoleKernel::class, function (ConsoleKernel $kernel, Application $app) { $kernel->whenCommandLifecycleIsLongerThan(-1, function () use ($app) { // @phpstan-ignore method.notFound - $app->make(Pulse::class)->store(); + $app->make(Pulse::class)->ingest(); }); }); }); diff --git a/tests/Feature/Commands/ClearCommandTest.php b/tests/Feature/Commands/ClearCommandTest.php index 8ffcde4d..3b93f771 100644 --- a/tests/Feature/Commands/ClearCommandTest.php +++ b/tests/Feature/Commands/ClearCommandTest.php @@ -7,17 +7,21 @@ it('clears Pulse data', function () { Pulse::set('foo', 'bar', 'baz'); Pulse::record('foo', 'bar', 123)->max()->count(); - Pulse::store(); + Pulse::ingest(); - expect(DB::table('pulse_values')->count())->toBe(1); - expect(DB::table('pulse_entries')->count())->toBe(1); - expect(DB::table('pulse_aggregates')->count())->toBe(8); + Pulse::ignore(function () { + expect(DB::table('pulse_values')->count())->toBe(1); + expect(DB::table('pulse_entries')->count())->toBe(1); + expect(DB::table('pulse_aggregates')->count())->toBe(8); + }); Artisan::call('pulse:clear'); - expect(DB::table('pulse_values')->count())->toBe(0); - expect(DB::table('pulse_entries')->count())->toBe(0); - expect(DB::table('pulse_aggregates')->count())->toBe(0); + Pulse::ignore(function () { + expect(DB::table('pulse_values')->count())->toBe(0); + expect(DB::table('pulse_entries')->count())->toBe(0); + expect(DB::table('pulse_aggregates')->count())->toBe(0); + }); }); it('can specify types', function () { @@ -25,18 +29,22 @@ Pulse::set('delete-me', 'foo', 'bar'); Pulse::record('keep-me', 'foo', 123)->max()->count(); Pulse::record('delete-me', 'foo', 123)->max()->count(); - Pulse::store(); + Pulse::ingest(); - expect(DB::table('pulse_values')->count())->toBe(2); - expect(DB::table('pulse_entries')->count())->toBe(2); - expect(DB::table('pulse_aggregates')->count())->toBe(16); + Pulse::ignore(function () { + expect(DB::table('pulse_values')->count())->toBe(2); + expect(DB::table('pulse_entries')->count())->toBe(2); + expect(DB::table('pulse_aggregates')->count())->toBe(16); + }); Artisan::call('pulse:clear --type delete-me'); - expect(DB::table('pulse_values')->where('type', 'keep-me')->count())->toBe(1); - expect(DB::table('pulse_values')->where('type', 'delete-me')->count())->toBe(0); - expect(DB::table('pulse_entries')->where('type', 'keep-me')->count())->toBe(1); - expect(DB::table('pulse_entries')->where('type', 'delete-me')->count())->toBe(0); - expect(DB::table('pulse_aggregates')->where('type', 'keep-me')->count())->toBe(8); - expect(DB::table('pulse_aggregates')->where('type', 'delete-me')->count())->toBe(0); + Pulse::ignore(function () { + expect(DB::table('pulse_values')->where('type', 'keep-me')->count())->toBe(1); + expect(DB::table('pulse_values')->where('type', 'delete-me')->count())->toBe(0); + expect(DB::table('pulse_entries')->where('type', 'keep-me')->count())->toBe(1); + expect(DB::table('pulse_entries')->where('type', 'delete-me')->count())->toBe(0); + expect(DB::table('pulse_aggregates')->where('type', 'keep-me')->count())->toBe(8); + expect(DB::table('pulse_aggregates')->where('type', 'delete-me')->count())->toBe(0); + }); }); diff --git a/tests/Feature/Ingests/DatabaseTest.php b/tests/Feature/Ingests/DatabaseTest.php index 0b1138c4..cae97b84 100644 --- a/tests/Feature/Ingests/DatabaseTest.php +++ b/tests/Feature/Ingests/DatabaseTest.php @@ -4,9 +4,11 @@ use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Date; use Illuminate\Support\Facades\DB; +use Laravel\Pulse\Facades\Pulse; use Laravel\Pulse\Storage\DatabaseStorage; it('trims values at or past expiry', function () { + Pulse::stopRecording(); Date::setTestNow('2000-01-08 00:00:05'); DB::table('pulse_values')->insert([ ['type' => 'type', 'key' => 'foo', 'value' => 'value', 'timestamp' => CarbonImmutable::parse('2000-01-01 00:00:04')->getTimestamp()], @@ -20,6 +22,7 @@ }); it('trims entries at or after week after timestamp', function () { + Pulse::stopRecording(); Date::setTestNow('2000-01-08 00:00:05'); DB::table('pulse_entries')->insert([ ['type' => 'foo', 'key' => 'xxxx', 'value' => 1, 'timestamp' => CarbonImmutable::parse('2000-01-01 00:00:04')->getTimestamp()], @@ -33,6 +36,7 @@ }); it('trims aggregates once the bucket is no longer relevant', function () { + Pulse::stopRecording(); Date::setTestNow('2000-01-08 01:01:05'); DB::table('pulse_aggregates')->insert([ diff --git a/tests/Feature/Livewire/CacheTest.php b/tests/Feature/Livewire/CacheTest.php index 65fac67d..80ddbbd2 100644 --- a/tests/Feature/Livewire/CacheTest.php +++ b/tests/Feature/Livewire/CacheTest.php @@ -37,7 +37,7 @@ Pulse::record('cache_miss', 'foo')->count(); Pulse::record('cache_miss', 'bar')->count(); - Pulse::store(); + Pulse::ingest(); Livewire::test(Cache::class, ['lazy' => false]) ->assertViewHas('allCacheInteractions', (object) [ @@ -55,7 +55,7 @@ Pulse::record('cache_hit', 'foo')->count()->onlyBuckets(); } Pulse::record('cache_miss', 'foo')->count()->onlyBuckets(); - Pulse::store(); + Pulse::ingest(); Livewire::test(Cache::class, ['lazy' => false]) ->assertDontSeeHtml("100.00%\n") @@ -65,7 +65,7 @@ it('does not show decimals for round numbers', function () { Pulse::record('cache_hit', 'foo')->count()->onlyBuckets(); Pulse::record('cache_miss', 'foo')->count()->onlyBuckets(); - Pulse::store(); + Pulse::ingest(); Livewire::test(Cache::class, ['lazy' => false]) ->assertDontSeeHtml("50.00%\n") diff --git a/tests/Feature/Livewire/ExceptionsTest.php b/tests/Feature/Livewire/ExceptionsTest.php index b0005a59..ca7dc458 100644 --- a/tests/Feature/Livewire/ExceptionsTest.php +++ b/tests/Feature/Livewire/ExceptionsTest.php @@ -32,7 +32,7 @@ Pulse::record('exception', $exception1, now()->timestamp)->max()->count(); Pulse::record('exception', $exception2, now()->timestamp)->max()->count(); - Pulse::store(); + Pulse::ingest(); Livewire::test(Exceptions::class, ['lazy' => false]) ->assertViewHas('exceptions', collect([ diff --git a/tests/Feature/Livewire/QueuesTest.php b/tests/Feature/Livewire/QueuesTest.php index 17f6594d..ac1e453f 100644 --- a/tests/Feature/Livewire/QueuesTest.php +++ b/tests/Feature/Livewire/QueuesTest.php @@ -33,7 +33,7 @@ Pulse::record('processed', 'database:default')->count()->onlyBuckets(); Pulse::record('released', 'database:default')->count()->onlyBuckets(); - Pulse::store(); + Pulse::ingest(); Livewire::test(Queues::class, ['lazy' => false]) ->assertViewHas('queues', collect([ diff --git a/tests/Feature/Livewire/ServersTest.php b/tests/Feature/Livewire/ServersTest.php index 541c10b4..f727a872 100644 --- a/tests/Feature/Livewire/ServersTest.php +++ b/tests/Feature/Livewire/ServersTest.php @@ -36,7 +36,7 @@ ], ])); - Pulse::store(); + Pulse::ingest(); Livewire::test(Servers::class, ['lazy' => false]) ->assertViewHas('servers', collect([ diff --git a/tests/Feature/Livewire/SlowJobsTest.php b/tests/Feature/Livewire/SlowJobsTest.php index 6d31b74d..d924a657 100644 --- a/tests/Feature/Livewire/SlowJobsTest.php +++ b/tests/Feature/Livewire/SlowJobsTest.php @@ -29,7 +29,7 @@ Pulse::record('slow_job', 'App\Jobs\MyJob', 1000)->max()->count(); Pulse::record('slow_job', 'App\Jobs\MyOtherJob', 1000)->max()->count(); - Pulse::store(); + Pulse::ingest(); Livewire::test(SlowJobs::class, ['lazy' => false]) ->assertViewHas('slowJobs', collect([ diff --git a/tests/Feature/Livewire/SlowOutgoingRequestsTest.php b/tests/Feature/Livewire/SlowOutgoingRequestsTest.php index cfc7fa7a..05bfdf51 100644 --- a/tests/Feature/Livewire/SlowOutgoingRequestsTest.php +++ b/tests/Feature/Livewire/SlowOutgoingRequestsTest.php @@ -29,7 +29,7 @@ Pulse::record('slow_outgoing_request', json_encode(['GET', 'http://example.com']), 1000)->max()->count(); Pulse::record('slow_outgoing_request', json_encode(['GET', 'http://example.org']), 1000)->max()->count(); - Pulse::store(); + Pulse::ingest(); Livewire::test(SlowOutgoingRequests::class, ['lazy' => false]) ->assertViewHas('slowOutgoingRequests', collect([ diff --git a/tests/Feature/Livewire/SlowQueriesTest.php b/tests/Feature/Livewire/SlowQueriesTest.php index 34dcfd09..207db56b 100644 --- a/tests/Feature/Livewire/SlowQueriesTest.php +++ b/tests/Feature/Livewire/SlowQueriesTest.php @@ -32,7 +32,7 @@ Pulse::record('slow_query', $query1, 1000)->max()->count(); Pulse::record('slow_query', $query2, 1000)->max()->count(); - Pulse::store(); + Pulse::ingest(); Livewire::test(SlowQueries::class, ['lazy' => false]) ->assertViewHas('slowQueries', collect([ diff --git a/tests/Feature/Livewire/SlowRequestsTest.php b/tests/Feature/Livewire/SlowRequestsTest.php index 4ec0ba0f..b1d808ef 100644 --- a/tests/Feature/Livewire/SlowRequestsTest.php +++ b/tests/Feature/Livewire/SlowRequestsTest.php @@ -32,7 +32,7 @@ Pulse::record('slow_request', $request1, 1000)->max()->count(); Pulse::record('slow_request', $request2, 1000)->max()->count(); - Pulse::store(); + Pulse::ingest(); Livewire::test(SlowRequests::class, ['lazy' => false]) ->assertViewHas('slowRequests', collect([ diff --git a/tests/Feature/Livewire/UsageTest.php b/tests/Feature/Livewire/UsageTest.php index 45799190..c8526eb1 100644 --- a/tests/Feature/Livewire/UsageTest.php +++ b/tests/Feature/Livewire/UsageTest.php @@ -42,7 +42,7 @@ Pulse::record($type, $users[1]->id)->count(); Pulse::record($type, $users[2]->id)->count(); - Pulse::store(); + Pulse::ingest(); Livewire::withQueryParams(['usage' => $query]) ->test(Usage::class, ['lazy' => false]) diff --git a/tests/Feature/PulseTest.php b/tests/Feature/PulseTest.php index 3199894b..3e9d8464 100644 --- a/tests/Feature/PulseTest.php +++ b/tests/Feature/PulseTest.php @@ -17,7 +17,7 @@ Pulse::record('foo', 'keep', 0); Pulse::set('baz', 'keep', ''); Pulse::set('baz', 'ignore', ''); - Pulse::store(); + Pulse::ingest(); expect($storage->stored)->toHaveCount(2); expect($storage->stored[0])->toBeInstanceOf(Entry::class); @@ -35,7 +35,7 @@ Pulse::set('value', 'lazy', '1'); }); Pulse::set('value', 'eager', '1'); - Pulse::store(); + Pulse::ingest(); expect($storage->stored)->toHaveCount(4); expect($storage->stored[0])->toBeInstanceOf(Entry::class); @@ -57,5 +57,5 @@ }); Pulse::flush(); - expect(Pulse::store())->toBe(0); + expect(Pulse::ingest())->toBe(0); }); diff --git a/tests/Feature/Recorders/CacheInteractionsTest.php b/tests/Feature/Recorders/CacheInteractionsTest.php index 62e19f76..94f048be 100644 --- a/tests/Feature/Recorders/CacheInteractionsTest.php +++ b/tests/Feature/Recorders/CacheInteractionsTest.php @@ -14,7 +14,7 @@ Cache::put('hit-key', 1); Cache::get('hit-key'); Cache::get('miss-key'); - Pulse::store(); + Pulse::ingest(); $entries = Pulse::ignore(fn () => DB::table('pulse_entries')->get()); expect($entries)->toHaveCount(2); @@ -53,20 +53,20 @@ it('ignores internal illuminate cache interactions', function () { Cache::get('illuminate:'); - expect(Pulse::store())->toBe(0); + expect(Pulse::ingest())->toBe(0); }); it('ignores internal pulse cache interactions', function () { Cache::get('laravel:pulse:'); - expect(Pulse::store())->toBe(0); + expect(Pulse::ingest())->toBe(0); }); it('stores the original keys by default', function () { Carbon::setTestNow('2000-01-02 03:04:05'); Cache::get('users:1234:profile'); - Pulse::store(); + Pulse::ingest(); $entries = Pulse::ignore(fn () => DB::table('pulse_entries')->get()); expect($entries)->toHaveCount(1); @@ -85,7 +85,7 @@ '/users:\d+:profile/' => 'users:{user}:profile', ]); Cache::get('users:1234:profile'); - Pulse::store(); + Pulse::ingest(); $entries = Pulse::ignore(fn () => DB::table('pulse_entries')->get()); expect($entries)->toHaveCount(1); @@ -104,7 +104,7 @@ '/^([^:]+):([^:]+):baz/' => '\2:\1', ]); Cache::get('foo:bar:baz'); - Pulse::store(); + Pulse::ingest(); $entries = Pulse::ignore(fn () => DB::table('pulse_entries')->get()); expect($entries)->toHaveCount(1); @@ -123,7 +123,7 @@ '/\d/' => 'foo', ]); Cache::get('actual-key'); - Pulse::store(); + Pulse::ingest(); $entries = Pulse::ignore(fn () => DB::table('pulse_entries')->get()); expect($entries)->toHaveCount(1); @@ -143,7 +143,7 @@ '/FOO/i' => 'uppercase-key', ]); Cache::get('FOO'); - Pulse::store(); + Pulse::ingest(); $entries = Pulse::ignore(fn () => DB::table('pulse_entries')->get()); expect($entries)->toHaveCount(1); @@ -162,7 +162,7 @@ Cache::get('laravel:pulse:foo:bar'); - expect(Pulse::store())->toBe(0); + expect(Pulse::ingest())->toBe(0); }); it('can sample', function () { @@ -179,13 +179,13 @@ Cache::get('foo'); Cache::get('foo'); - expect(Pulse::store())->toEqualWithDelta(1, 4); + expect(Pulse::ingest())->toEqualWithDelta(1, 4); }); it('groups job exception keys', function () { Cache::get('job-exceptions:'.Str::uuid()); - Pulse::store(); + Pulse::ingest(); $entries = Pulse::ignore(fn () => DB::table('pulse_entries')->get()); expect($entries)->toHaveCount(1); diff --git a/tests/Feature/Recorders/ExceptionsTest.php b/tests/Feature/Recorders/ExceptionsTest.php index 83b35b82..f21dbf79 100644 --- a/tests/Feature/Recorders/ExceptionsTest.php +++ b/tests/Feature/Recorders/ExceptionsTest.php @@ -11,7 +11,7 @@ report(new RuntimeException('Expected exception.')); - expect(Pulse::store())->toBe(1); + expect(Pulse::ingest())->toBe(1); $entries = Pulse::ignore(fn () => DB::table('pulse_entries')->get()); expect($entries)->toHaveCount(1); @@ -51,7 +51,7 @@ Carbon::setTestNow('2000-01-02 03:04:05'); report(new RuntimeException('Expected exception.')); - Pulse::store(); + Pulse::ingest(); $entries = Pulse::ignore(fn () => DB::table('pulse_entries')->get()); expect($entries)->toHaveCount(1); @@ -88,7 +88,7 @@ Carbon::setTestNow('2000-01-01 00:00:00'); Pulse::report(new MyReportedException('Hello, Pulse!')); - Pulse::store(); + Pulse::ingest(); $entries = Pulse::ignore(fn () => DB::table('pulse_entries')->get()); expect($entries)->toHaveCount(1); @@ -109,7 +109,7 @@ report(new \Tests\Feature\Exceptions\MyException('Ignored exception')); - expect(Pulse::store())->toBe(0); + expect(Pulse::ingest())->toBe(0); }); it('can sample', function () { @@ -126,7 +126,7 @@ report(new MyReportedException()); report(new MyReportedException()); - expect(Pulse::store())->toEqualWithDelta(1, 4); + expect(Pulse::ingest())->toEqualWithDelta(1, 4); }); it('can sample at zero', function () { @@ -143,7 +143,7 @@ report(new MyReportedException()); report(new MyReportedException()); - expect(Pulse::store())->toBe(0); + expect(Pulse::ingest())->toBe(0); }); it('can sample at one', function () { @@ -160,7 +160,7 @@ report(new MyReportedException()); report(new MyReportedException()); - expect(Pulse::store())->toBe(10); + expect(Pulse::ingest())->toBe(10); }); class MyReportedException extends Exception diff --git a/tests/Feature/Recorders/QueuesTest.php b/tests/Feature/Recorders/QueuesTest.php index 11101fab..6174ac9c 100644 --- a/tests/Feature/Recorders/QueuesTest.php +++ b/tests/Feature/Recorders/QueuesTest.php @@ -37,7 +37,7 @@ function queueAggregates() Bus::dispatchToQueue(new MyJob); - Pulse::store(); + Pulse::ingest(); $aggregates = queueAggregates(); expect($aggregates)->toHaveCount(4); @@ -56,7 +56,7 @@ function queueAggregates() throw new RuntimeException('Nope'); }); - Pulse::store(); + Pulse::ingest(); $aggregates = queueAggregates(); expect($aggregates)->toHaveCount(4); @@ -71,7 +71,8 @@ function queueAggregates() * Work the job for the first time. */ Artisan::call('queue:work', ['--max-jobs' => 1, '--tries' => 2, '--stop-when-empty' => true, '--sleep' => 0]); - expect(Queue::size())->toBe(1); + Pulse::ignore(fn () => expect(Queue::size())->toBe(1)); + $aggregates = queueAggregates(); expect($aggregates)->toHaveCount(12); expect($aggregates)->toContainAggregateForAllPeriods( @@ -85,7 +86,7 @@ function queueAggregates() * Work the job for the second time. */ Artisan::call('queue:work', ['--max-jobs' => 1, '--tries' => 2, '--stop-when-empty' => true, '--sleep' => 0]); - expect(Queue::size())->toBe(0); + Pulse::ignore(fn () => expect(Queue::size())->toBe(0)); $aggregates = queueAggregates(); expect($aggregates)->toHaveCount(16); @@ -107,7 +108,7 @@ function queueAggregates() Config::set('queue.default', 'database'); Queue::push('MyJob'); - Pulse::store(); + Pulse::ingest(); $aggregates = queueAggregates(); expect($aggregates)->toHaveCount(4); @@ -127,7 +128,7 @@ function queueAggregates() * Dispatch the event. */ MyEvent::dispatch(); - Pulse::store(); + Pulse::ingest(); $aggregates = queueAggregates(); expect($aggregates)->toHaveCount(4); @@ -142,7 +143,7 @@ function queueAggregates() * Work the job for the first time. */ Artisan::call('queue:work', ['--max-jobs' => 1, '--tries' => 2, '--stop-when-empty' => true, '--sleep' => 0]); - expect(Queue::size())->toBe(1); + Pulse::ignore(fn () => expect(Queue::size())->toBe(1)); $aggregates = queueAggregates(); expect($aggregates)->toHaveCount(12); expect($aggregates)->toContainAggregateForAllPeriods( @@ -156,7 +157,7 @@ function queueAggregates() * Work the job for the second time. */ Artisan::call('queue:work', ['--max-jobs' => 1, '--tries' => 2, '--stop-when-empty' => true, '--sleep' => 0]); - expect(Queue::size())->toBe(0); + Pulse::ignore(fn () => expect(Queue::size())->toBe(0)); $aggregates = queueAggregates(); expect($aggregates)->toHaveCount(16); expect($aggregates)->toContainAggregateForAllPeriods( @@ -180,7 +181,7 @@ function queueAggregates() * Dispatch the mail. */ Mail::to('test@example.com')->queue(new MyMailThatFails); - Pulse::store(); + Pulse::ingest(); $aggregates = queueAggregates(); expect($aggregates)->toHaveCount(4); @@ -195,7 +196,7 @@ function queueAggregates() * Work the job for the first time. */ Artisan::call('queue:work', ['--max-jobs' => 1, '--tries' => 2, '--stop-when-empty' => true, '--sleep' => 0]); - expect(Queue::size())->toBe(1); + Pulse::ignore(fn () => expect(Queue::size())->toBe(1)); $aggregates = queueAggregates(); expect($aggregates)->toHaveCount(12); @@ -210,7 +211,7 @@ function queueAggregates() * Work the job for the second time. */ Artisan::call('queue:work', ['--max-jobs' => 1, '--tries' => 2, '--stop-when-empty' => true, '--sleep' => 0]); - expect(Queue::size())->toBe(0); + Pulse::ignore(fn () => expect(Queue::size())->toBe(0)); $aggregates = queueAggregates(); expect($aggregates)->toHaveCount(16); expect($aggregates)->toContainAggregateForAllPeriods( @@ -234,7 +235,7 @@ function queueAggregates() * Dispatch the notification. */ Notification::route('mail', 'test@example.com')->notify(new MyNotificationThatFails); - Pulse::store(); + Pulse::ingest(); $aggregates = queueAggregates(); expect($aggregates)->toHaveCount(4); @@ -249,7 +250,7 @@ function queueAggregates() * Work the job for the first time. */ Artisan::call('queue:work', ['--max-jobs' => 1, '--tries' => 2, '--stop-when-empty' => true, '--sleep' => 0]); - expect(Queue::size())->toBe(1); + Pulse::ignore(fn () => expect(Queue::size())->toBe(1)); $aggregates = queueAggregates(); expect($aggregates)->toHaveCount(12); expect($aggregates)->toContainAggregateForAllPeriods( @@ -263,7 +264,7 @@ function queueAggregates() * Work the job for the second time. */ Artisan::call('queue:work', ['--max-jobs' => 1, '--tries' => 2, '--stop-when-empty' => true, '--sleep' => 0]); - expect(Queue::size())->toBe(0); + Pulse::ignore(fn () => expect(Queue::size())->toBe(0)); $aggregates = queueAggregates(); expect($aggregates)->toHaveCount(16); expect($aggregates)->toContainAggregateForAllPeriods( @@ -287,7 +288,7 @@ function queueAggregates() * Dispatch the command. */ Artisan::queue(MyCommandThatFails::class); - Pulse::store(); + Pulse::ingest(); $aggregates = queueAggregates(); expect($aggregates)->toHaveCount(4); @@ -302,7 +303,7 @@ function queueAggregates() * Work the job for the first time. */ Artisan::call('queue:work', ['--max-jobs' => 1, '--tries' => 2, '--stop-when-empty' => true, '--sleep' => 0]); - expect(Queue::size())->toBe(1); + Pulse::ignore(fn () => expect(Queue::size())->toBe(1)); $aggregates = queueAggregates(); expect($aggregates)->toHaveCount(12); expect($aggregates)->toContainAggregateForAllPeriods( @@ -316,7 +317,7 @@ function queueAggregates() * Work the job for the second time. */ Artisan::call('queue:work', ['--max-jobs' => 1, '--tries' => 2, '--stop-when-empty' => true, '--sleep' => 0]); - expect(Queue::size())->toBe(0); + Pulse::ignore(fn () => expect(Queue::size())->toBe(0)); $aggregates = queueAggregates(); expect($aggregates)->toHaveCount(16); expect($aggregates)->toContainAggregateForAllPeriods( @@ -340,9 +341,9 @@ function queueAggregates() * Dispatch the job. */ Bus::dispatchToQueue(new MyJobWithMultipleAttemptsThatAlwaysThrows); - Pulse::store(); + Pulse::ingest(); - expect(Queue::size())->toBe(1); + Pulse::ignore(fn () => expect(Queue::size())->toBe(1)); $aggregates = queueAggregates(); expect($aggregates)->toHaveCount(4); expect($aggregates)->toContainAggregateForAllPeriods( @@ -357,7 +358,7 @@ function queueAggregates() */ Artisan::call('queue:work', ['--max-jobs' => 1, '--stop-when-empty' => true, '--sleep' => 0]); - expect(Queue::size())->toBe(1); + Pulse::ignore(fn () => expect(Queue::size())->toBe(1)); $aggregates = queueAggregates(); expect($aggregates)->toHaveCount(12); expect($aggregates)->toContainAggregateForAllPeriods( @@ -372,7 +373,7 @@ function queueAggregates() */ Artisan::call('queue:work', ['--max-jobs' => 1, '--stop-when-empty' => true, '--sleep' => 0]); - expect(Queue::size())->toBe(1); + Pulse::ignore(fn () => expect(Queue::size())->toBe(1)); $aggregates = queueAggregates(); expect($aggregates)->toHaveCount(12); expect($aggregates)->toContainAggregateForAllPeriods( @@ -393,7 +394,7 @@ function queueAggregates() */ Artisan::call('queue:work', ['--max-jobs' => 1, '--stop-when-empty' => true, '--sleep' => 0]); - expect(Queue::size())->toBe(0); + Pulse::ignore(fn () => expect(Queue::size())->toBe(0)); $aggregates = queueAggregates(); expect($aggregates)->toHaveCount(16); expect($aggregates)->toContainAggregateForAllPeriods( @@ -423,9 +424,9 @@ function queueAggregates() * Dispatch the job. */ Bus::dispatchToQueue(new MyJobThatPassesOnTheSecondAttempt); - Pulse::store(); + Pulse::ingest(); - expect(Queue::size())->toBe(1); + Pulse::ignore(fn () => expect(Queue::size())->toBe(1)); $aggregates = queueAggregates(); expect($aggregates)->toHaveCount(4); expect($aggregates)->toContainAggregateForAllPeriods( @@ -440,7 +441,7 @@ function queueAggregates() */ Artisan::call('queue:work', ['--max-jobs' => 1, '--stop-when-empty' => true, '--sleep' => 0]); - expect(Queue::size())->toBe(1); + Pulse::ignore(fn () => expect(Queue::size())->toBe(1)); $aggregates = queueAggregates(); expect($aggregates)->toHaveCount(12); expect($aggregates)->toContainAggregateForAllPeriods( @@ -455,7 +456,7 @@ function queueAggregates() */ Artisan::call('queue:work', ['--max-jobs' => 1, '--stop-when-empty' => true, '--sleep' => 0]); - expect(Queue::size())->toBe(0); + Pulse::ignore(fn () => expect(Queue::size())->toBe(0)); $aggregates = queueAggregates(); expect($aggregates)->toHaveCount(16); expect($aggregates)->toContainAggregateForAllPeriods( @@ -479,9 +480,9 @@ function queueAggregates() * Dispatch the job. */ Bus::dispatchToQueue(new MyJobThatManuallyFails); - Pulse::store(); + Pulse::ingest(); - expect(Queue::size())->toBe(1); + Pulse::ignore(fn () => expect(Queue::size())->toBe(1)); $aggregates = queueAggregates(); expect($aggregates)->toHaveCount(4); expect($aggregates)->toContainAggregateForAllPeriods( @@ -498,7 +499,7 @@ function queueAggregates() app(ExceptionHandler::class)->reportable(fn (\Throwable $e) => throw $e); Artisan::call('queue:work', ['--max-jobs' => 1, '--stop-when-empty' => true, '--sleep' => 0]); app()->forgetInstance(ExceptionHandler::class); - expect(Queue::size())->toBe(0); + Pulse::ignore(fn () => expect(Queue::size())->toBe(0)); $aggregates = queueAggregates(); expect($aggregates)->toHaveCount(16); expect($aggregates)->toContainAggregateForAllPeriods( @@ -516,14 +517,14 @@ function queueAggregates() ]); MyJobThatPassesOnTheSecondAttempt::$attempts = 0; Bus::dispatchToQueue(new MyJobThatPassesOnTheSecondAttempt); - expect(Queue::size())->toBe(1); + Pulse::ignore(fn () => expect(Queue::size())->toBe(1)); /* * Work the job for the first time. */ Artisan::call('queue:work', ['--max-jobs' => 1, '--stop-when-empty' => true, '--sleep' => 0]); - expect(Queue::size())->toBe(1); + Pulse::ignore(fn () => expect(Queue::size())->toBe(1)); expect(queueAggregates())->toHaveCount(0); /* @@ -531,7 +532,7 @@ function queueAggregates() */ Artisan::call('queue:work', ['--max-jobs' => 1, '--stop-when-empty' => true, '--sleep' => 0]); - expect(Queue::size())->toBe(0); + Pulse::ignore(fn () => expect(Queue::size())->toBe(0)); expect(queueAggregates())->toHaveCount(0); }); @@ -549,9 +550,9 @@ function queueAggregates() Bus::dispatchToQueue(new MyJob); Bus::dispatchToQueue(new MyJob); Bus::dispatchToQueue(new MyJob); - Pulse::store(); + Pulse::ingest(); - expect(Queue::size())->toBe(10); + Pulse::ignore(fn () => expect(Queue::size())->toBe(10)); expect(queueAggregates()->count())->toEqualWithDelta(1, 4); }); @@ -570,8 +571,8 @@ function queueAggregates() Bus::dispatchToQueue(new MyJob); Bus::dispatchToQueue(new MyJob); - expect(Queue::size())->toBe(10); - expect(Pulse::store())->toBe(0); + Pulse::ignore(fn () => expect(Queue::size())->toBe(10)); + expect(Pulse::ingest())->toBe(0); }); it('can sample at one', function () { @@ -589,8 +590,8 @@ function queueAggregates() Bus::dispatchToQueue(new MyJob); Bus::dispatchToQueue(new MyJob); - expect(Queue::size())->toBe(10); - expect(Pulse::store())->toBe(10); + Pulse::ignore(fn () => expect(Queue::size())->toBe(10)); + expect(Pulse::ingest())->toBe(10); }); it("doesn't sample subsequent events for jobs that aren't initially sampled", function () { @@ -603,9 +604,9 @@ function queueAggregates() Bus::dispatchToQueue(new MyJobThatAlwaysFails); Bus::dispatchToQueue(new MyJobThatAlwaysFails); - Pulse::store(); + Pulse::ingest(); - expect(Queue::size())->toBe(2); + Pulse::ignore(fn () => expect(Queue::size())->toBe(2)); $aggregates = queueAggregates(); expect($aggregates)->toHaveCount(4); expect($aggregates)->toContainAggregateForAllPeriods( @@ -616,7 +617,7 @@ function queueAggregates() ); Artisan::call('queue:work', ['--tries' => 2, '--max-jobs' => 4, '--stop-when-empty' => true, '--sleep' => 0]); - expect(Queue::size())->toBe(0); + Pulse::ignore(fn () => expect(Queue::size())->toBe(0)); $aggregates = queueAggregates(); expect($aggregates)->toHaveCount(16); expect($aggregates)->toContainAggregateForAllPeriods( @@ -638,8 +639,8 @@ function queueAggregates() Config::set('queue.connections.database.queue', 'custom-default'); Bus::dispatchToQueue(new MyJob); - Pulse::store(); - expect(Queue::size())->toBe(1); + Pulse::ingest(); + Pulse::ignore(fn () => expect(Queue::size())->toBe(1)); $aggregates = queueAggregates(); expect($aggregates)->toHaveCount(4); diff --git a/tests/Feature/Recorders/SlowJobsTest.php b/tests/Feature/Recorders/SlowJobsTest.php index 928c4ef2..d792baab 100644 --- a/tests/Feature/Recorders/SlowJobsTest.php +++ b/tests/Feature/Recorders/SlowJobsTest.php @@ -22,9 +22,9 @@ Carbon::setTestNow('2000-01-02 03:04:05'); Bus::dispatchToQueue(new MySlowJob); - Pulse::store(); + Pulse::ingest(); - expect(Queue::size())->toBe(1); + Pulse::ignore(fn () => expect(Queue::size())->toBe(1)); expect(Pulse::ignore(fn () => DB::table('pulse_aggregates')->where('type', 'slow_job')->get()))->toHaveCount(0); /* @@ -33,7 +33,7 @@ Carbon::setTestNow('2000-01-02 03:04:10'); Artisan::call('queue:work', ['--max-jobs' => 1, '--stop-when-empty' => true, '--sleep' => 0]); - expect(Queue::size())->toBe(0); + Pulse::ignore(fn () => expect(Queue::size())->toBe(0)); $entries = Pulse::ignore(fn () => DB::table('pulse_entries')->where('type', 'slow_job')->get()); expect($entries)->toHaveCount(1); expect($entries[0])->toHaveProperties([ @@ -69,9 +69,9 @@ Carbon::setTestNow('2000-01-02 03:04:05'); Bus::dispatchToQueue(new MySlowJob); - Pulse::store(); + Pulse::ingest(); - expect(Queue::size())->toBe(1); + Pulse::ignore(fn () => expect(Queue::size())->toBe(1)); expect(Pulse::ignore(fn () => DB::table('pulse_aggregates')->where('type', 'slow_job')->get()))->toHaveCount(0); /* @@ -80,7 +80,7 @@ Carbon::setTestNow('2000-01-02 03:04:10'); Artisan::call('queue:work', ['--max-jobs' => 1, '--stop-when-empty' => true, '--sleep' => 0]); - expect(Queue::size())->toBe(0); + Pulse::ignore(fn () => expect(Queue::size())->toBe(0)); expect(Pulse::ignore(fn () => DB::table('pulse_entries')->where('type', 'slow_job')->get()))->toHaveCount(0); expect(Pulse::ignore(fn () => DB::table('pulse_aggregates')->where('type', 'slow_job')->get()))->toHaveCount(0); }); @@ -97,9 +97,9 @@ */ Bus::dispatchToQueue(new MySlowJob); - Pulse::store(); + Pulse::ingest(); - expect(Queue::size())->toBe(1); + Pulse::ignore(fn () => expect(Queue::size())->toBe(1)); expect(Pulse::ignore(fn () => DB::table('pulse_aggregates')->where('type', 'slow_job')->get()))->toHaveCount(0); /* @@ -108,7 +108,7 @@ Artisan::call('queue:work', ['--max-jobs' => 1, '--stop-when-empty' => true, '--sleep' => 0]); - expect(Queue::size())->toBe(0); + Pulse::ignore(fn () => expect(Queue::size())->toBe(0)); expect(Pulse::ignore(fn () => DB::table('pulse_entries')->where('type', 'slow_job')->get()))->toHaveCount(0); expect(Pulse::ignore(fn () => DB::table('pulse_aggregates')->where('type', 'slow_job')->get()))->toHaveCount(0); }); @@ -132,9 +132,9 @@ Bus::dispatchToQueue(new MySlowJob); Bus::dispatchToQueue(new MySlowJob); Bus::dispatchToQueue(new MySlowJob); - Pulse::store(); + Pulse::ingest(); - expect(Queue::size())->toBe(10); + Pulse::ignore(fn () => expect(Queue::size())->toBe(10)); /* * Work the jobs. @@ -142,7 +142,7 @@ Artisan::call('queue:work', ['--stop-when-empty' => true, '--sleep' => 0]); - expect(Queue::size())->toBe(0); + Pulse::ignore(fn () => expect(Queue::size())->toBe(0)); expect(Pulse::ignore(fn () => DB::table('pulse_entries')->where('type', 'slow_job')->count()))->toEqualWithDelta(1, 4); Pulse::flush(); @@ -167,9 +167,9 @@ Bus::dispatchToQueue(new MySlowJob); Bus::dispatchToQueue(new MySlowJob); Bus::dispatchToQueue(new MySlowJob); - Pulse::store(); + Pulse::ingest(); - expect(Queue::size())->toBe(10); + Pulse::ignore(fn () => expect(Queue::size())->toBe(10)); /* * Work the jobs. @@ -177,7 +177,7 @@ Artisan::call('queue:work', ['--stop-when-empty' => true, '--sleep' => 0]); - expect(Queue::size())->toBe(0); + Pulse::ignore(fn () => expect(Queue::size())->toBe(0)); expect(Pulse::ignore(fn () => DB::table('pulse_entries')->where('type', 'slow_job')->count()))->toBe(0); Pulse::flush(); @@ -202,9 +202,9 @@ Bus::dispatchToQueue(new MySlowJob); Bus::dispatchToQueue(new MySlowJob); Bus::dispatchToQueue(new MySlowJob); - Pulse::store(); + Pulse::ingest(); - expect(Queue::size())->toBe(10); + Pulse::ignore(fn () => expect(Queue::size())->toBe(10)); /* * Work the jobs. @@ -212,7 +212,7 @@ Artisan::call('queue:work', ['--stop-when-empty' => true, '--sleep' => 0]); - expect(Queue::size())->toBe(0); + Pulse::ignore(fn () => expect(Queue::size())->toBe(0)); expect(Pulse::ignore(fn () => DB::table('pulse_entries')->where('type', 'slow_job')->count()))->toBe(10); Pulse::flush(); diff --git a/tests/Feature/Recorders/SlowOutgoingRequestsTest.php b/tests/Feature/Recorders/SlowOutgoingRequestsTest.php index d134a931..7c56edb7 100644 --- a/tests/Feature/Recorders/SlowOutgoingRequestsTest.php +++ b/tests/Feature/Recorders/SlowOutgoingRequestsTest.php @@ -13,7 +13,7 @@ Http::fake(fn () => Http::response('ok')); Http::get('https://laravel.com'); - Pulse::store(); + Pulse::ingest(); $entries = Pulse::ignore(fn () => DB::table('pulse_entries')->get()); expect($entries)->toHaveCount(1); @@ -48,7 +48,7 @@ Http::get('https://laravel.com'); - expect(Pulse::store())->toBe(0); + expect(Pulse::ingest())->toBe(0); }); it('captures failed requests', function () { @@ -57,7 +57,7 @@ Http::fake(['https://laravel.com' => Http::response('error', status: 500)]); Http::get('https://laravel.com'); - Pulse::store(); + Pulse::ingest(); $entries = Pulse::ignore(fn () => DB::table('pulse_entries')->get()); expect($entries)->toHaveCount(1); @@ -75,7 +75,7 @@ Http::fake(['https://laravel.com*' => Http::response('ok')]); Http::get('https://laravel.com?foo=123'); - Pulse::store(); + Pulse::ingest(); $entries = Pulse::ignore(fn () => DB::table('pulse_entries')->get()); expect($entries)->toHaveCount(1); @@ -96,7 +96,7 @@ '#^https://github\.com/([^/]+)/([^/]+)/commits/([^/]+)$#' => 'github.com/{user}/{repo}/commits/{branch}', ]); Http::get('https://github.com/laravel/pulse/commits/1.x'); - Pulse::store(); + Pulse::ingest(); $entries = Pulse::ignore(fn () => DB::table('pulse_entries')->get()); expect($entries)->toHaveCount(1); @@ -117,7 +117,7 @@ '#^https?://([^/]+).*$#' => '\1/*', ]); Http::get('https://github.com/laravel/pulse/commits/1.x'); - Pulse::store(); + Pulse::ingest(); $entries = Pulse::ignore(fn () => DB::table('pulse_entries')->get()); expect($entries)->toHaveCount(1); @@ -139,7 +139,7 @@ '/PARAMETER/i' => 'uppercase-parameter', ]); Http::get('https://github.com?PARAMETER=123'); - Pulse::store(); + Pulse::ingest(); $entries = Pulse::ignore(fn () => DB::table('pulse_entries')->get()); expect($entries)->toHaveCount(1); @@ -160,7 +160,7 @@ Http::get('http://127.0.0.1:13714/render'); - expect(Pulse::store())->toBe(0); + expect(Pulse::ingest())->toBe(0); }); it('can sample', function () { @@ -179,7 +179,7 @@ Http::get('http://example.com'); Http::get('http://example.com'); - expect(Pulse::store())->toEqualWithDelta(1, 4); + expect(Pulse::ingest())->toEqualWithDelta(1, 4); }); it('can sample at zero', function () { @@ -198,7 +198,7 @@ Http::get('http://example.com'); Http::get('http://example.com'); - expect(Pulse::store())->toBe(0); + expect(Pulse::ingest())->toBe(0); }); it('can sample at one', function () { @@ -217,5 +217,5 @@ Http::get('http://example.com'); Http::get('http://example.com'); - expect(Pulse::store())->toBe(10); + expect(Pulse::ingest())->toBe(10); }); diff --git a/tests/Feature/Recorders/SlowQueriesTest.php b/tests/Feature/Recorders/SlowQueriesTest.php index d4d2a7bf..14f06dbe 100644 --- a/tests/Feature/Recorders/SlowQueriesTest.php +++ b/tests/Feature/Recorders/SlowQueriesTest.php @@ -16,7 +16,7 @@ DB::connection()->statement('select * from users'); - Pulse::store(); + Pulse::ingest(); $entries = Pulse::ignore(fn () => DB::table('pulse_entries')->get()); expect($entries)->toHaveCount(1); @@ -60,7 +60,7 @@ }); DB::connection()->statement('select * from users'); - Pulse::store(); + Pulse::ingest(); $entries = Pulse::ignore(fn () => DB::table('pulse_entries')->get()); expect($entries)->toHaveCount(1); @@ -103,7 +103,7 @@ }); DB::table('users')->count(); - Pulse::store(); + Pulse::ingest(); Pulse::ignore(fn () => expect(DB::table('pulse_entries')->count())->toBe(0)); }); @@ -115,7 +115,7 @@ }); DB::table('users')->count(); - Pulse::store(); + Pulse::ingest(); Pulse::ignore(fn () => expect(DB::table('pulse_entries')->count())->toBe(1)); }); @@ -127,7 +127,7 @@ }); DB::table('users')->count(); - Pulse::store(); + Pulse::ingest(); Pulse::ignore(fn () => expect(DB::table('pulse_entries')->count())->toBe(1)); }); @@ -138,8 +138,7 @@ '/(["`])pulse_[\w]+?\1/', // Pulse tables ]); - expect(Pulse::store())->toBe(0); - DB::table('pulse_entries')->count(); + expect(Pulse::ingest())->toBe(0); }); it('can sample', function () { @@ -157,7 +156,7 @@ DB::table('users')->count(); DB::table('users')->count(); - expect(Pulse::store())->toEqualWithDelta(1, 4); + expect(Pulse::ingest())->toEqualWithDelta(1, 4); }); it('can sample at zero', function () { @@ -175,7 +174,7 @@ DB::table('users')->count(); DB::table('users')->count(); - expect(Pulse::store())->toBe(0); + expect(Pulse::ingest())->toBe(0); }); it('can sample at one', function () { @@ -193,5 +192,5 @@ DB::table('users')->count(); DB::table('users')->count(); - expect(Pulse::store())->toBe(10); + expect(Pulse::ingest())->toBe(10); }); diff --git a/tests/Feature/RedisTest.php b/tests/Feature/RedisTest.php index 24ab03f2..54b2894c 100644 --- a/tests/Feature/RedisTest.php +++ b/tests/Feature/RedisTest.php @@ -76,7 +76,7 @@ ->output(); [$firstEntryKey, $lastEntryKey] = collect(explode("\n", $output))->only([17, 21])->values(); - $commands = captureRedisCommands(fn () => $ingest->store(new StorageFake())); + $commands = captureRedisCommands(fn () => $ingest->digest(new StorageFake())); expect($commands)->toContain('"XRANGE" "laravel_database_laravel:pulse:ingest" "-" "+" "COUNT" "567"'); expect($commands)->toContain('"XDEL" "laravel_database_laravel:pulse:ingest" "'.$firstEntryKey.'" "'.$lastEntryKey.'"'); diff --git a/tests/Feature/Storage/DatabaseStorageTest.php b/tests/Feature/Storage/DatabaseStorageTest.php index 966de06e..a58857e0 100644 --- a/tests/Feature/Storage/DatabaseStorageTest.php +++ b/tests/Feature/Storage/DatabaseStorageTest.php @@ -2,6 +2,7 @@ use Carbon\CarbonInterval; use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\DB; use Laravel\Pulse\Facades\Pulse; @@ -9,7 +10,7 @@ Pulse::record('type', 'key1', 200)->count()->min()->max()->sum()->avg(); Pulse::record('type', 'key1', 100)->count()->min()->max()->sum()->avg(); Pulse::record('type', 'key2', 400)->count()->min()->max()->sum()->avg(); - Pulse::store(); + Pulse::ingest(); $entries = Pulse::ignore(fn () => DB::table('pulse_entries')->orderBy('id')->get()); expect($entries)->toHaveCount(3); @@ -64,7 +65,7 @@ expect($aggregates[39])->toHaveProperties(['type' => 'type', 'period' => 10080, 'aggregate' => 'sum', 'key' => 'key2', 'value' => 400]); Pulse::record('type', 'key1', 600)->count()->min()->max()->sum()->avg(); - Pulse::store(); + Pulse::ingest(); $entries = Pulse::ignore(fn () => DB::table('pulse_entries')->orderBy('id')->get()); expect($entries)->toHaveCount(4); @@ -121,6 +122,7 @@ }); it('combines duplicate count aggregates before upserting', function () { + Config::set('pulse.ingest.trim.lottery', [0, 1]); $queries = collect(); DB::listen(fn ($query) => $queries[] = $query); @@ -128,7 +130,7 @@ Pulse::record('type', 'key1')->count(); Pulse::record('type', 'key1')->count(); Pulse::record('type', 'key2')->count(); - Pulse::store(); + Pulse::ingest(); expect($queries)->toHaveCount(2); expect($queries[0]->sql)->toContain('pulse_entries'); @@ -142,6 +144,7 @@ }); it('combines duplicate min aggregates before upserting', function () { + Config::set('pulse.ingest.trim.lottery', [0, 1]); $queries = collect(); DB::listen(fn ($query) => $queries[] = $query); @@ -149,7 +152,7 @@ Pulse::record('type', 'key1', 100)->min(); Pulse::record('type', 'key1', 300)->min(); Pulse::record('type', 'key2', 100)->min(); - Pulse::store(); + Pulse::ingest(); expect($queries)->toHaveCount(2); expect($queries[0]->sql)->toContain('pulse_entries'); @@ -163,6 +166,7 @@ }); it('combines duplicate max aggregates before upserting', function () { + Config::set('pulse.ingest.trim.lottery', [0, 1]); $queries = collect(); DB::listen(fn ($query) => $queries[] = $query); @@ -170,7 +174,7 @@ Pulse::record('type', 'key1', 300)->max(); Pulse::record('type', 'key1', 200)->max(); Pulse::record('type', 'key2', 100)->max(); - Pulse::store(); + Pulse::ingest(); expect($queries)->toHaveCount(2); expect($queries[0]->sql)->toContain('pulse_entries'); @@ -184,6 +188,7 @@ }); it('combines duplicate sum aggregates before upserting', function () { + Config::set('pulse.ingest.trim.lottery', [0, 1]); $queries = collect(); DB::listen(fn ($query) => $queries[] = $query); @@ -191,7 +196,7 @@ Pulse::record('type', 'key1', 300)->sum(); Pulse::record('type', 'key1', 200)->sum(); Pulse::record('type', 'key2', 100)->sum(); - Pulse::store(); + Pulse::ingest(); expect($queries)->toHaveCount(2); expect($queries[0]->sql)->toContain('pulse_entries'); @@ -205,6 +210,7 @@ }); it('combines duplicate average aggregates before upserting', function () { + Config::set('pulse.ingest.trim.lottery', [0, 1]); $queries = collect(); DB::listen(fn ($query) => $queries[] = $query); @@ -212,7 +218,7 @@ Pulse::record('type', 'key1', 300)->avg(); Pulse::record('type', 'key1', 200)->avg(); Pulse::record('type', 'key2', 100)->avg(); - Pulse::store(); + Pulse::ingest(); expect($queries)->toHaveCount(2); expect($queries[0]->sql)->toContain('pulse_entries'); @@ -229,7 +235,7 @@ Pulse::record('type', 'key1', 400)->avg(); Pulse::record('type', 'key1', 400)->avg(); Pulse::record('type', 'key1', 400)->avg(); - Pulse::store(); + Pulse::ingest(); $aggregate = Pulse::ignore(fn () => DB::table('pulse_aggregates')->where('period', 60)->where('key', 'key1')->first()); expect($aggregate->count)->toEqual(6); expect($aggregate->value)->toEqual(300); @@ -268,7 +274,7 @@ Pulse::record('slow_request', 'GET /bar', 400)->min()->max()->sum()->avg()->count(); Pulse::record('slow_request', 'GET /bar', 600)->min()->max()->sum()->avg()->count(); - Pulse::store(); + Pulse::ingest(); Carbon::setTestNow('2000-01-01 13:00:00'); @@ -328,7 +334,7 @@ Pulse::record('cache_hit', 'user:*')->count(); Pulse::record('cache_miss', 'user:*')->count(); - Pulse::store(); + Pulse::ingest(); Carbon::setTestNow('2000-01-01 13:00:00'); @@ -382,7 +388,7 @@ Pulse::record('cache_hit', 'flight:*')->count(); Pulse::record('cache_miss', 'flight:*')->count(); - Pulse::store(); + Pulse::ingest(); Carbon::setTestNow('2000-01-01 13:00:00'); diff --git a/tests/Pest.php b/tests/Pest.php index 66aa0b9c..e3a5b3b3 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -33,13 +33,13 @@ Pulse::flush(); Pulse::handleExceptionsUsing(fn (Throwable $e) => throw $e); Gate::define('viewPulse', fn ($user = null) => true); - Config::set('pulse.ingest.trim.lottery', [0, 1]); + Config::set('pulse.ingest.trim.lottery', [1, 1]); }) ->afterEach(function () { Str::createUuidsNormally(); - if (Pulse::store() > 0) { - throw new RuntimeException('The queue is not empty'); + if (Pulse::wantsIngesting()) { + throw new RuntimeException('There are pending entries.'); } }) ->in('Unit', 'Feature'); From 9db0f799a7dde1940b75c205165527a56e2bdfa4 Mon Sep 17 00:00:00 2001 From: taylorotwell Date: Fri, 15 Dec 2023 13:54:37 +0000 Subject: [PATCH 090/110] Update facade docblocks --- src/Facades/Pulse.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Facades/Pulse.php b/src/Facades/Pulse.php index 8dc9ca2e..aa21047b 100644 --- a/src/Facades/Pulse.php +++ b/src/Facades/Pulse.php @@ -17,6 +17,7 @@ * @method static \Laravel\Pulse\Pulse filter(callable $filter) * @method static int ingest() * @method static int digest() + * @method static bool wantsIngesting() * @method static \Illuminate\Support\Collection recorders() * @method static \Illuminate\Support\Collection resolveUsers(\Illuminate\Support\Collection $ids) * @method static \Laravel\Pulse\Pulse users(callable $callback) @@ -34,6 +35,7 @@ * @method static mixed rescue(callable $callback) * @method static \Laravel\Pulse\Pulse setContainer(\Illuminate\Contracts\Foundation\Application $container) * @method static void afterResolving(\Illuminate\Contracts\Foundation\Application $app, string $class, \Closure $callback) + * @method static void store(\Illuminate\Support\Collection $items) * @method static void trim() * @method static void purge(array $types = null) * @method static \Illuminate\Support\Collection values(string $type, array $keys = null) From 53a5f27b3540f6a4fa72802350a78ef8a22e9391 Mon Sep 17 00:00:00 2001 From: Dries Vints Date: Mon, 18 Dec 2023 09:39:53 +0100 Subject: [PATCH 091/110] Update 1_Bug_report.yml --- .github/ISSUE_TEMPLATE/1_Bug_report.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/1_Bug_report.yml b/.github/ISSUE_TEMPLATE/1_Bug_report.yml index d288f66c..75e35e3a 100644 --- a/.github/ISSUE_TEMPLATE/1_Bug_report.yml +++ b/.github/ISSUE_TEMPLATE/1_Bug_report.yml @@ -7,8 +7,8 @@ body: - type: input attributes: label: Pulse Version - description: Provide the Pulse version that you are using. - placeholder: 1.0.0 + description: Provide the **EXACT* Pulse (beta) version that you are using. + placeholder: 1.0.0-beta.5 validations: required: true - type: input From 24775434b1899a0ba55682a2a9cf8a4305ee7cb5 Mon Sep 17 00:00:00 2001 From: Dries Vints Date: Mon, 18 Dec 2023 09:40:04 +0100 Subject: [PATCH 092/110] Update 1_Bug_report.yml --- .github/ISSUE_TEMPLATE/1_Bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/1_Bug_report.yml b/.github/ISSUE_TEMPLATE/1_Bug_report.yml index 75e35e3a..2bd300ec 100644 --- a/.github/ISSUE_TEMPLATE/1_Bug_report.yml +++ b/.github/ISSUE_TEMPLATE/1_Bug_report.yml @@ -7,7 +7,7 @@ body: - type: input attributes: label: Pulse Version - description: Provide the **EXACT* Pulse (beta) version that you are using. + description: Provide the **EXACT** Pulse (beta) version that you are using. placeholder: 1.0.0-beta.5 validations: required: true From 9978c4a872e90dcba5bea8eaa0eeb2c301a2ecb9 Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Tue, 19 Dec 2023 08:38:57 +1100 Subject: [PATCH 093/110] Fix tests --- tests/Feature/Recorders/ServersTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Feature/Recorders/ServersTest.php b/tests/Feature/Recorders/ServersTest.php index 5ef4874c..90406c65 100644 --- a/tests/Feature/Recorders/ServersTest.php +++ b/tests/Feature/Recorders/ServersTest.php @@ -11,7 +11,7 @@ Config::set('pulse.recorders.'.Servers::class.'.server_name', 'Foo'); Date::setTestNow(Date::now()->startOfMinute()); event(app(SharedBeat::class)); - Pulse::store(); + Pulse::ingest(); expect(Pulse::ignore(fn () => DB::table('pulse_entries')->count()))->toBe(0); From 3eb58f7e59e61ab9595170db9072e32d4c76a986 Mon Sep 17 00:00:00 2001 From: Jess Archer Date: Tue, 19 Dec 2023 11:35:33 +1000 Subject: [PATCH 094/110] [1.x] Add SQLite support for local environments (#235) * Add SQLite support for local environments * Update test matrix --- .github/workflows/tests.yml | 47 ++++++- .../2023_06_07_000001_create_pulse_tables.php | 3 + src/Storage/DatabaseStorage.php | 36 ++++- src/Support/PulseMigration.php | 2 +- tests/Feature/Ingests/DatabaseTest.php | 130 +++++++++++++----- tests/Feature/Recorders/SlowRequestsTest.php | 40 +++--- tests/Feature/Storage/DatabaseStorageTest.php | 45 ++++-- tests/Pest.php | 1 + 8 files changed, 231 insertions(+), 73 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 14a758a7..b2bcbf15 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -85,8 +85,9 @@ jobs: strategy: fail-fast: true matrix: - php: [8.2, 8.3] + php: [8.3] laravel: [10] + stability: [prefer-stable] name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} - Stability ${{ matrix.stability }} - PostgreSQL 14 @@ -116,3 +117,47 @@ jobs: env: DB_CONNECTION: pgsql DB_PASSWORD: password + + sqlite: + runs-on: ubuntu-22.04 + + services: + redis: + image: redis + ports: + - 6379:6379 + options: --entrypoint redis-server + + strategy: + fail-fast: true + matrix: + php: [8.3] + laravel: [10] + stability: [prefer-stable] + + name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} - Stability ${{ matrix.stability }} - SQLite + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: dom, curl, libxml, mbstring, redis, pcntl, zip + ini-values: error_reporting=E_ALL + tools: composer:v2 + coverage: none + + - name: Install redis-cli + run: sudo apt-get install -qq redis-tools + + - name: Install dependencies + run: | + composer update --prefer-dist --no-interaction --no-progress --${{ matrix.stability }} + + - name: Execute tests + run: vendor/bin/pest + env: + DB_CONNECTION: sqlite 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 38c7fdde..f75b51e4 100644 --- a/database/migrations/2023_06_07_000001_create_pulse_tables.php +++ b/database/migrations/2023_06_07_000001_create_pulse_tables.php @@ -23,6 +23,7 @@ public function up(): void match ($this->driver()) { 'mysql' => $table->char('key_hash', 16)->charset('binary')->virtualAs('unhex(md5(`key`))'), 'pgsql' => $table->uuid('key_hash')->storedAs('md5("key")::uuid'), + 'sqlite' => $table->string('key_hash'), }; $table->mediumText('value'); @@ -39,6 +40,7 @@ public function up(): void match ($this->driver()) { 'mysql' => $table->char('key_hash', 16)->charset('binary')->virtualAs('unhex(md5(`key`))'), 'pgsql' => $table->uuid('key_hash')->storedAs('md5("key")::uuid'), + 'sqlite' => $table->string('key_hash'), }; $table->bigInteger('value')->nullable(); @@ -57,6 +59,7 @@ public function up(): void match ($this->driver()) { 'mysql' => $table->char('key_hash', 16)->charset('binary')->virtualAs('unhex(md5(`key`))'), 'pgsql' => $table->uuid('key_hash')->storedAs('md5("key")::uuid'), + 'sqlite' => $table->string('key_hash'), }; $table->string('aggregate'); $table->decimal('value', 20, 2); diff --git a/src/Storage/DatabaseStorage.php b/src/Storage/DatabaseStorage.php index 15942bea..4456f082 100644 --- a/src/Storage/DatabaseStorage.php +++ b/src/Storage/DatabaseStorage.php @@ -53,7 +53,14 @@ public function store(Collection $items): void ->chunk($this->config->get('pulse.storage.database.chunk')) ->each(fn ($chunk) => $this->connection() ->table('pulse_entries') - ->insert($chunk->map->attributes()->all()) + ->insert( + $this->requiresManualKeyHash() + ? $chunk->map(fn ($entry) => [ + ...($attributes = $entry->attributes()), + 'key_hash' => md5($attributes['key']), + ])->all() + : $chunk->map->attributes()->all() + ) ); [$counts, $minimums, $maximums, $sums, $averages] = array_values($entries @@ -96,7 +103,12 @@ public function store(Collection $items): void ->each(fn ($chunk) => $this->connection() ->table('pulse_values') ->upsert( - $chunk->map->attributes()->all(), + $this->requiresManualKeyHash() + ? $chunk->map(fn ($entry) => [ + ...($attributes = $entry->attributes()), + 'key_hash' => md5($attributes['key']), + ])->all() + : $chunk->map->attributes()->all(), ['type', 'key_hash'], ['timestamp', 'value'] ) @@ -169,7 +181,7 @@ protected function upsertCount(array $values): int [ 'value' => match ($driver = $this->connection()->getDriverName()) { 'mysql' => new Expression('`value` + values(`value`)'), - 'pgsql' => new Expression('"pulse_aggregates"."value" + "excluded"."value"'), + 'pgsql', 'sqlite' => new Expression('"pulse_aggregates"."value" + "excluded"."value"'), default => throw new RuntimeException("Unsupported database driver [{$driver}]"), }, ] @@ -190,6 +202,7 @@ protected function upsertMin(array $values): int 'value' => match ($driver = $this->connection()->getDriverName()) { 'mysql' => new Expression('least(`value`, values(`value`))'), 'pgsql' => new Expression('least("pulse_aggregates"."value", "excluded"."value")'), + 'sqlite' => new Expression('min("pulse_aggregates"."value", "excluded"."value")'), default => throw new RuntimeException("Unsupported database driver [{$driver}]"), }, ] @@ -210,6 +223,7 @@ protected function upsertMax(array $values): int 'value' => match ($driver = $this->connection()->getDriverName()) { 'mysql' => new Expression('greatest(`value`, values(`value`))'), 'pgsql' => new Expression('greatest("pulse_aggregates"."value", "excluded"."value")'), + 'sqlite' => new Expression('max("pulse_aggregates"."value", "excluded"."value")'), default => throw new RuntimeException("Unsupported database driver [{$driver}]"), }, ] @@ -229,7 +243,7 @@ protected function upsertSum(array $values): int [ 'value' => match ($driver = $this->connection()->getDriverName()) { 'mysql' => new Expression('`value` + values(`value`)'), - 'pgsql' => new Expression('"pulse_aggregates"."value" + "excluded"."value"'), + 'pgsql', 'sqlite' => new Expression('"pulse_aggregates"."value" + "excluded"."value"'), default => throw new RuntimeException("Unsupported database driver [{$driver}]"), }, ] @@ -251,7 +265,7 @@ protected function upsertAvg(array $values): int 'value' => new Expression('(`value` * `count` + (values(`value`) * values(`count`))) / (`count` + values(`count`))'), 'count' => new Expression('`count` + values(`count`)'), ], - 'pgsql' => [ + 'pgsql', 'sqlite' => [ 'value' => new Expression('("pulse_aggregates"."value" * "pulse_aggregates"."count" + ("excluded"."value" * "excluded"."count")) / ("pulse_aggregates"."count" + "excluded"."count")'), 'count' => new Expression('"pulse_aggregates"."count" + "excluded"."count"'), ], @@ -366,6 +380,10 @@ protected function preaggregate(Collection $entries, string $aggregate, Closure 'aggregate' => $aggregate, 'key' => $entry->key, ], $entry); + + if ($this->requiresManualKeyHash()) { + $aggregates[$key]['key_hash'] = md5($entry->key); + } } else { $aggregates[$key] = $callback($aggregates[$key], $entry); } @@ -762,4 +780,12 @@ protected function wrap(string $value): string { return $this->connection()->getQueryGrammar()->wrap($value); } + + /** + * Determine whether a manually generated key hash is required. + */ + protected function requiresManualKeyHash(): bool + { + return $this->connection()->getDriverName() === 'sqlite'; + } } diff --git a/src/Support/PulseMigration.php b/src/Support/PulseMigration.php index ceb75211..fcdf6279 100644 --- a/src/Support/PulseMigration.php +++ b/src/Support/PulseMigration.php @@ -24,7 +24,7 @@ public function getConnection(): ?string */ protected function shouldRun(): bool { - if (in_array($this->driver(), ['mysql', 'pgsql'])) { + if (in_array($this->driver(), ['mysql', 'pgsql', 'sqlite'])) { return true; } diff --git a/tests/Feature/Ingests/DatabaseTest.php b/tests/Feature/Ingests/DatabaseTest.php index cae97b84..9fd0500c 100644 --- a/tests/Feature/Ingests/DatabaseTest.php +++ b/tests/Feature/Ingests/DatabaseTest.php @@ -1,6 +1,5 @@ insert([ - ['type' => 'type', 'key' => 'foo', 'value' => 'value', 'timestamp' => CarbonImmutable::parse('2000-01-01 00:00:04')->getTimestamp()], - ['type' => 'type', 'key' => 'bar', 'value' => 'value', 'timestamp' => CarbonImmutable::parse('2000-01-01 00:00:05')->getTimestamp()], - ['type' => 'type', 'key' => 'baz', 'value' => 'value', 'timestamp' => CarbonImmutable::parse('2000-01-01 00:00:06')->getTimestamp()], - ]); - App::make(DatabaseStorage::class)->trim(); expect(DB::table('pulse_values')->pluck('key')->all())->toBe(['baz']); }); it('trims entries at or after week after timestamp', function () { + Date::setTestNow('2000-01-01 00:00:04'); + Pulse::record('foo', 'xxxx', 1); + Date::setTestNow('2000-01-01 00:00:05'); + Pulse::record('bar', 'xxxx', 1); + Date::setTestNow('2000-01-01 00:00:06'); + Pulse::record('baz', 'xxxx', 1); + Pulse::ingest(); + Pulse::stopRecording(); Date::setTestNow('2000-01-08 00:00:05'); - DB::table('pulse_entries')->insert([ - ['type' => 'foo', 'key' => 'xxxx', 'value' => 1, 'timestamp' => CarbonImmutable::parse('2000-01-01 00:00:04')->getTimestamp()], - ['type' => 'bar', 'key' => 'xxxx', 'value' => 1, 'timestamp' => CarbonImmutable::parse('2000-01-01 00:00:05')->getTimestamp()], - ['type' => 'baz', 'key' => 'xxxx', 'value' => 1, 'timestamp' => CarbonImmutable::parse('2000-01-01 00:00:06')->getTimestamp()], - ]); - App::make(DatabaseStorage::class)->trim(); expect(DB::table('pulse_entries')->pluck('type')->all())->toBe(['baz']); }); -it('trims aggregates once the bucket is no longer relevant', function () { +it('trims aggregates once the 1 hour bucket is no longer relevant', function () { + Date::setTestNow('2000-01-01 00:00:59'); // Bucket: 2000-01-01 00:00:00 + Pulse::record('foo', 'xxxx', 1)->count(); + Pulse::ingest(); + expect(Pulse::ignore(fn () => DB::table('pulse_aggregates')->where('period', 60)->count()))->toBe(1); + + Date::setTestNow('2000-01-01 00:01:00'); // Bucket: 2000-01-01 00:01:00 + Pulse::record('foo', 'xxxx', 1)->count(); + Pulse::ingest(); + expect(Pulse::ignore(fn () => DB::table('pulse_aggregates')->where('period', 60)->count()))->toBe(2); + + Pulse::stopRecording(); + Date::setTestNow('2000-01-01 00:59:59'); // 1 second before the oldest bucket become irrelevant. + App::make(DatabaseStorage::class)->trim(); + expect(DB::table('pulse_aggregates')->where('period', 60)->count())->toBe(2); + + Date::setTestNow('2000-01-01 01:00:00'); // The second the oldest bucket become irrelevant. + App::make(DatabaseStorage::class)->trim(); + expect(DB::table('pulse_aggregates')->where('period', 60)->count())->toBe(1); +}); + +it('trims aggregates once the 6 hour bucket is no longer relevant', function () { + Date::setTestNow('2000-01-01 00:05:59'); // Bucket: 2000-01-01 00:00:00 + Pulse::record('foo', 'xxxx', 1)->count(); + Pulse::ingest(); + expect(Pulse::ignore(fn () => DB::table('pulse_aggregates')->where('period', 360)->count()))->toBe(1); + + Date::setTestNow('2000-01-01 00:06:00'); // Bucket: 2000-01-01 00:06:00 + Pulse::record('foo', 'xxxx', 1)->count(); + Pulse::ingest(); + expect(Pulse::ignore(fn () => DB::table('pulse_aggregates')->where('period', 360)->count()))->toBe(2); + + Pulse::stopRecording(); + Date::setTestNow('2000-01-01 05:59:59'); // 1 second before the oldest bucket become irrelevant. + App::make(DatabaseStorage::class)->trim(); + expect(DB::table('pulse_aggregates')->where('period', 360)->count())->toBe(2); + + Date::setTestNow('2000-01-01 06:00:00'); // The second the oldest bucket become irrelevant. + App::make(DatabaseStorage::class)->trim(); + expect(DB::table('pulse_aggregates')->where('period', 360)->count())->toBe(1); +}); + +it('trims aggregates once the 24 hour bucket is no longer relevant', function () { + Date::setTestNow('2000-01-01 00:23:59'); // Bucket: 2000-01-01 00:00:00 + Pulse::record('foo', 'xxxx', 1)->count(); + Pulse::ingest(); + expect(Pulse::ignore(fn () => DB::table('pulse_aggregates')->where('period', 1440)->count()))->toBe(1); + + Date::setTestNow('2000-01-01 00:24:00'); // Bucket: 2000-01-01 00:24:00 + Pulse::record('foo', 'xxxx', 1)->count(); + Pulse::ingest(); + expect(Pulse::ignore(fn () => DB::table('pulse_aggregates')->where('period', 1440)->count()))->toBe(2); + Pulse::stopRecording(); - Date::setTestNow('2000-01-08 01:01:05'); - - DB::table('pulse_aggregates')->insert([ - ['period' => 60, 'type' => 'foo:60', 'key' => 'xxxx', 'aggregate' => 'sum', 'value' => 1, 'count' => 1, 'bucket' => CarbonImmutable::parse('2000-01-08 00:01:04')->getTimestamp()], - ['period' => 60, 'type' => 'bar:60', 'key' => 'xxxx', 'aggregate' => 'sum', 'value' => 1, 'count' => 1, 'bucket' => CarbonImmutable::parse('2000-01-08 00:01:05')->getTimestamp()], - ['period' => 60, 'type' => 'baz:60', 'key' => 'xxxx', 'aggregate' => 'sum', 'value' => 1, 'count' => 1, 'bucket' => CarbonImmutable::parse('2000-01-08 00:01:06')->getTimestamp()], - ['period' => 360, 'type' => 'foo:360', 'key' => 'xxxx', 'aggregate' => 'sum', 'value' => 1, 'count' => 1, 'bucket' => CarbonImmutable::parse('2000-01-07 19:01:04')->getTimestamp()], - ['period' => 360, 'type' => 'bar:360', 'key' => 'xxxx', 'aggregate' => 'sum', 'value' => 1, 'count' => 1, 'bucket' => CarbonImmutable::parse('2000-01-07 19:01:05')->getTimestamp()], - ['period' => 360, 'type' => 'baz:360', 'key' => 'xxxx', 'aggregate' => 'sum', 'value' => 1, 'count' => 1, 'bucket' => CarbonImmutable::parse('2000-01-07 19:01:06')->getTimestamp()], - ['period' => 1440, 'type' => 'foo:1440', 'key' => 'xxxx', 'aggregate' => 'sum', 'value' => 1, 'count' => 1, 'bucket' => CarbonImmutable::parse('2000-01-07 01:01:04')->getTimestamp()], - ['period' => 1440, 'type' => 'bar:1440', 'key' => 'xxxx', 'aggregate' => 'sum', 'value' => 1, 'count' => 1, 'bucket' => CarbonImmutable::parse('2000-01-07 01:01:05')->getTimestamp()], - ['period' => 1440, 'type' => 'baz:1440', 'key' => 'xxxx', 'aggregate' => 'sum', 'value' => 1, 'count' => 1, 'bucket' => CarbonImmutable::parse('2000-01-07 01:01:06')->getTimestamp()], - ['period' => 10080, 'type' => 'foo:10080', 'key' => 'xxxx', 'aggregate' => 'sum', 'value' => 1, 'count' => 1, 'bucket' => CarbonImmutable::parse('2000-01-01 01:01:04')->getTimestamp()], - ['period' => 10080, 'type' => 'bar:10080', 'key' => 'xxxx', 'aggregate' => 'sum', 'value' => 1, 'count' => 1, 'bucket' => CarbonImmutable::parse('2000-01-01 01:01:05')->getTimestamp()], - ['period' => 10080, 'type' => 'baz:10080', 'key' => 'xxxx', 'aggregate' => 'sum', 'value' => 1, 'count' => 1, 'bucket' => CarbonImmutable::parse('2000-01-01 01:01:06')->getTimestamp()], - ]); + Date::setTestNow('2000-01-01 23:35:59'); // 1 second before the oldest bucket become irrelevant. + App::make(DatabaseStorage::class)->trim(); + expect(DB::table('pulse_aggregates')->where('period', 1440)->count())->toBe(2); + Date::setTestNow('2000-01-02 00:00:00'); // The second the oldest bucket become irrelevant. App::make(DatabaseStorage::class)->trim(); + expect(DB::table('pulse_aggregates')->where('period', 1440)->count())->toBe(1); +}); + +it('trims aggregates once the 7 day bucket is no longer relevant', function () { + Date::setTestNow('2000-01-01 02:23:59'); // Bucket: 1999-12-31 23:36:00 + Pulse::record('foo', 'xxxx', 1)->count(); + Pulse::ingest(); + expect(Pulse::ignore(fn () => DB::table('pulse_aggregates')->where('period', 10080)->count()))->toBe(1); - expect(DB::table('pulse_aggregates')->pluck('type')->all())->toEqualCanonicalizing([ - 'baz:60', - 'baz:360', - 'baz:1440', - 'baz:10080', - ]); + Date::setTestNow('2000-01-01 02:24:00'); // Bucket: 2000-01-01 02:24:00 + Pulse::record('foo', 'xxxx', 1)->count(); + Pulse::ingest(); + expect(Pulse::ignore(fn () => DB::table('pulse_aggregates')->where('period', 10080)->count()))->toBe(2); + + Pulse::stopRecording(); + Date::setTestNow('2000-01-07 23:35:59'); // 1 second before the oldest bucket become irrelevant. + App::make(DatabaseStorage::class)->trim(); + expect(DB::table('pulse_aggregates')->where('period', 10080)->count())->toBe(2); + + Date::setTestNow('2000-01-07 23:36:00'); // The second the oldest bucket become irrelevant. + App::make(DatabaseStorage::class)->trim(); + expect(DB::table('pulse_aggregates')->where('period', 10080)->count())->toBe(1); }); diff --git a/tests/Feature/Recorders/SlowRequestsTest.php b/tests/Feature/Recorders/SlowRequestsTest.php index 6e93ed48..e686822d 100644 --- a/tests/Feature/Recorders/SlowRequestsTest.php +++ b/tests/Feature/Recorders/SlowRequestsTest.php @@ -40,7 +40,7 @@ expect($aggregates[0]->aggregate)->toBe('count'); expect($aggregates[0]->key)->toBe(json_encode(['GET', '/test-route', 'Closure'])); expect($aggregates[0]->key_hash)->toBe(keyHash(json_encode(['GET', '/test-route', 'Closure']))); - expect($aggregates[0]->value)->toBe('1.00'); + expect($aggregates[0]->value)->toEqual(1); expect($aggregates[1]->bucket)->toBe(946782240); expect($aggregates[1]->period)->toBe(60); @@ -48,7 +48,7 @@ expect($aggregates[1]->aggregate)->toBe('max'); expect($aggregates[1]->key)->toBe(json_encode(['GET', '/test-route', 'Closure'])); expect($aggregates[1]->key_hash)->toBe(keyHash(json_encode(['GET', '/test-route', 'Closure']))); - expect($aggregates[1]->value)->toBe('4000.00'); + expect($aggregates[1]->value)->toEqual(4000); expect($aggregates[2]->bucket)->toBe(946782000); expect($aggregates[2]->period)->toBe(360); @@ -56,7 +56,7 @@ expect($aggregates[2]->aggregate)->toBe('count'); expect($aggregates[2]->key)->toBe(json_encode(['GET', '/test-route', 'Closure'])); expect($aggregates[2]->key_hash)->toBe(keyHash(json_encode(['GET', '/test-route', 'Closure']))); - expect($aggregates[2]->value)->toBe('1.00'); + expect($aggregates[2]->value)->toEqual(1); expect($aggregates[3]->bucket)->toBe(946782000); expect($aggregates[3]->period)->toBe(360); @@ -64,7 +64,7 @@ expect($aggregates[3]->aggregate)->toBe('max'); expect($aggregates[3]->key)->toBe(json_encode(['GET', '/test-route', 'Closure'])); expect($aggregates[3]->key_hash)->toBe(keyHash(json_encode(['GET', '/test-route', 'Closure']))); - expect($aggregates[3]->value)->toBe('4000.00'); + expect($aggregates[3]->value)->toEqual(4000); expect($aggregates[4]->bucket)->toBe(946781280); expect($aggregates[4]->period)->toBe(1440); @@ -72,7 +72,7 @@ expect($aggregates[4]->aggregate)->toBe('count'); expect($aggregates[4]->key)->toBe(json_encode(['GET', '/test-route', 'Closure'])); expect($aggregates[4]->key_hash)->toBe(keyHash(json_encode(['GET', '/test-route', 'Closure']))); - expect($aggregates[4]->value)->toBe('1.00'); + expect($aggregates[4]->value)->toEqual(1); expect($aggregates[5]->bucket)->toBe(946781280); expect($aggregates[5]->period)->toBe(1440); @@ -80,21 +80,21 @@ expect($aggregates[5]->aggregate)->toBe('max'); expect($aggregates[5]->key)->toBe(json_encode(['GET', '/test-route', 'Closure'])); expect($aggregates[5]->key_hash)->toBe(keyHash(json_encode(['GET', '/test-route', 'Closure']))); - expect($aggregates[5]->value)->toBe('4000.00'); + expect($aggregates[5]->value)->toEqual(4000); expect($aggregates[6]->period)->toBe(10080); expect($aggregates[6]->type)->toBe('slow_request'); expect($aggregates[6]->aggregate)->toBe('count'); expect($aggregates[6]->key)->toBe(json_encode(['GET', '/test-route', 'Closure'])); expect($aggregates[6]->key_hash)->toBe(keyHash(json_encode(['GET', '/test-route', 'Closure']))); - expect($aggregates[6]->value)->toBe('1.00'); + expect($aggregates[6]->value)->toEqual(1); expect($aggregates[7]->period)->toBe(10080); expect($aggregates[7]->type)->toBe('slow_request'); expect($aggregates[7]->aggregate)->toBe('max'); expect($aggregates[7]->key)->toBe(json_encode(['GET', '/test-route', 'Closure'])); expect($aggregates[7]->key_hash)->toBe(keyHash(json_encode(['GET', '/test-route', 'Closure']))); - expect($aggregates[7]->value)->toBe('4000.00'); + expect($aggregates[7]->value)->toEqual(4000); Pulse::ignore(fn () => expect(DB::table('pulse_values')->count())->toBe(0)); }); @@ -126,7 +126,7 @@ expect($aggregates[0]->aggregate)->toBe('count'); expect($aggregates[0]->key)->toBe('4321'); expect($aggregates[0]->key_hash)->toBe(keyHash('4321')); - expect($aggregates[0]->value)->toBe('1.00'); + expect($aggregates[0]->value)->toEqual(1); expect($aggregates[1]->bucket)->toBe(946782000); expect($aggregates[1]->period)->toBe(360); @@ -134,7 +134,7 @@ expect($aggregates[1]->aggregate)->toBe('count'); expect($aggregates[1]->key)->toBe('4321'); expect($aggregates[1]->key_hash)->toBe(keyHash('4321')); - expect($aggregates[1]->value)->toBe('1.00'); + expect($aggregates[1]->value)->toEqual(1); expect($aggregates[2]->bucket)->toBe(946781280); expect($aggregates[2]->period)->toBe(1440); @@ -142,14 +142,14 @@ expect($aggregates[2]->aggregate)->toBe('count'); expect($aggregates[2]->key)->toBe('4321'); expect($aggregates[2]->key_hash)->toBe(keyHash('4321')); - expect($aggregates[2]->value)->toBe('1.00'); + expect($aggregates[2]->value)->toEqual(1); expect($aggregates[3]->period)->toBe(10080); expect($aggregates[3]->type)->toBe('slow_user_request'); expect($aggregates[3]->aggregate)->toBe('count'); expect($aggregates[3]->key)->toBe('4321'); expect($aggregates[3]->key_hash)->toBe(keyHash('4321')); - expect($aggregates[3]->value)->toBe('1.00'); + expect($aggregates[3]->value)->toEqual(1); Pulse::ignore(fn () => expect(DB::table('pulse_values')->count())->toBe(0)); }); @@ -280,7 +280,7 @@ expect($aggregates[0]->aggregate)->toBe('count'); expect($aggregates[0]->key)->toBe(json_encode(['POST', '/test-route', 'via /livewire/update'])); expect($aggregates[0]->key_hash)->toBe(keyHash(json_encode(['POST', '/test-route', 'via /livewire/update']))); - expect($aggregates[0]->value)->toBe('1.00'); + expect($aggregates[0]->value)->toEqual(1); expect($aggregates[1]->bucket)->toBe(946782240); expect($aggregates[1]->period)->toBe(60); @@ -288,7 +288,7 @@ expect($aggregates[1]->aggregate)->toBe('max'); expect($aggregates[1]->key)->toBe(json_encode(['POST', '/test-route', 'via /livewire/update'])); expect($aggregates[1]->key_hash)->toBe(keyHash(json_encode(['POST', '/test-route', 'via /livewire/update']))); - expect($aggregates[1]->value)->toBe('4000.00'); + expect($aggregates[1]->value)->toEqual(4000); expect($aggregates[2]->bucket)->toBe(946782000); expect($aggregates[2]->period)->toBe(360); @@ -296,7 +296,7 @@ expect($aggregates[2]->aggregate)->toBe('count'); expect($aggregates[2]->key)->toBe(json_encode(['POST', '/test-route', 'via /livewire/update'])); expect($aggregates[2]->key_hash)->toBe(keyHash(json_encode(['POST', '/test-route', 'via /livewire/update']))); - expect($aggregates[2]->value)->toBe('1.00'); + expect($aggregates[2]->value)->toEqual(1); expect($aggregates[3]->bucket)->toBe(946782000); expect($aggregates[3]->period)->toBe(360); @@ -304,7 +304,7 @@ expect($aggregates[3]->aggregate)->toBe('max'); expect($aggregates[3]->key)->toBe(json_encode(['POST', '/test-route', 'via /livewire/update'])); expect($aggregates[3]->key_hash)->toBe(keyHash(json_encode(['POST', '/test-route', 'via /livewire/update']))); - expect($aggregates[3]->value)->toBe('4000.00'); + expect($aggregates[3]->value)->toEqual(4000); expect($aggregates[4]->bucket)->toBe(946781280); expect($aggregates[4]->period)->toBe(1440); @@ -312,7 +312,7 @@ expect($aggregates[4]->aggregate)->toBe('count'); expect($aggregates[4]->key)->toBe(json_encode(['POST', '/test-route', 'via /livewire/update'])); expect($aggregates[4]->key_hash)->toBe(keyHash(json_encode(['POST', '/test-route', 'via /livewire/update']))); - expect($aggregates[4]->value)->toBe('1.00'); + expect($aggregates[4]->value)->toEqual(1); expect($aggregates[5]->bucket)->toBe(946781280); expect($aggregates[5]->period)->toBe(1440); @@ -320,7 +320,7 @@ expect($aggregates[5]->aggregate)->toBe('max'); expect($aggregates[5]->key)->toBe(json_encode(['POST', '/test-route', 'via /livewire/update'])); expect($aggregates[5]->key_hash)->toBe(keyHash(json_encode(['POST', '/test-route', 'via /livewire/update']))); - expect($aggregates[5]->value)->toBe('4000.00'); + expect($aggregates[5]->value)->toEqual(4000); expect($aggregates[6]->bucket)->toBe(946774080); expect($aggregates[6]->period)->toBe(10080); @@ -328,7 +328,7 @@ expect($aggregates[6]->aggregate)->toBe('count'); expect($aggregates[6]->key)->toBe(json_encode(['POST', '/test-route', 'via /livewire/update'])); expect($aggregates[6]->key_hash)->toBe(keyHash(json_encode(['POST', '/test-route', 'via /livewire/update']))); - expect($aggregates[6]->value)->toBe('1.00'); + expect($aggregates[6]->value)->toEqual(1); expect($aggregates[7]->bucket)->toBe(946774080); expect($aggregates[7]->period)->toBe(10080); @@ -336,7 +336,7 @@ expect($aggregates[7]->aggregate)->toBe('max'); expect($aggregates[7]->key)->toBe(json_encode(['POST', '/test-route', 'via /livewire/update'])); expect($aggregates[7]->key_hash)->toBe(keyHash(json_encode(['POST', '/test-route', 'via /livewire/update']))); - expect($aggregates[7]->value)->toBe('4000.00'); + expect($aggregates[7]->value)->toEqual(4000); Pulse::ignore(fn () => expect(DB::table('pulse_values')->count())->toBe(0)); }); diff --git a/tests/Feature/Storage/DatabaseStorageTest.php b/tests/Feature/Storage/DatabaseStorageTest.php index a58857e0..0a9b1d48 100644 --- a/tests/Feature/Storage/DatabaseStorageTest.php +++ b/tests/Feature/Storage/DatabaseStorageTest.php @@ -135,8 +135,13 @@ expect($queries)->toHaveCount(2); expect($queries[0]->sql)->toContain('pulse_entries'); expect($queries[1]->sql)->toContain('pulse_aggregates'); - expect($queries[0]->bindings)->toHaveCount(4 * 4); // 4 entries, 4 columns each - expect($queries[1]->bindings)->toHaveCount(2 * 6 * 4); // 2 entries, 6 columns each, 4 periods + if (DB::connection()->getDriverName() === 'sqlite') { + expect($queries[0]->bindings)->toHaveCount(4 * 5); // 4 entries, 5 columns each + expect($queries[1]->bindings)->toHaveCount(2 * 7 * 4); // 2 entries, 7 columns each, 4 periods + } else { + expect($queries[0]->bindings)->toHaveCount(4 * 4); // 4 entries, 4 columns each + expect($queries[1]->bindings)->toHaveCount(2 * 6 * 4); // 2 entries, 6 columns each, 4 periods + } $aggregates = Pulse::ignore(fn () => DB::table('pulse_aggregates')->where('period', 60)->orderBy('key')->pluck('value', 'key')); expect($aggregates['key1'])->toEqual(3); @@ -157,8 +162,13 @@ expect($queries)->toHaveCount(2); expect($queries[0]->sql)->toContain('pulse_entries'); expect($queries[1]->sql)->toContain('pulse_aggregates'); - expect($queries[0]->bindings)->toHaveCount(4 * 4); // 4 entries, 4 columns each - expect($queries[1]->bindings)->toHaveCount(2 * 6 * 4); // 2 entries, 6 columns each, 4 periods + if (DB::connection()->getDriverName() === 'sqlite') { + expect($queries[0]->bindings)->toHaveCount(4 * 5); // 4 entries, 5 columns each + expect($queries[1]->bindings)->toHaveCount(2 * 7 * 4); // 2 entries, 7 columns each, 4 periods + } else { + expect($queries[0]->bindings)->toHaveCount(4 * 4); // 4 entries, 4 columns each + expect($queries[1]->bindings)->toHaveCount(2 * 6 * 4); // 2 entries, 6 columns each, 4 periods + } $aggregates = Pulse::ignore(fn () => DB::table('pulse_aggregates')->where('period', 60)->orderBy('key')->pluck('value', 'key')); expect($aggregates['key1'])->toEqual(100); @@ -179,8 +189,13 @@ expect($queries)->toHaveCount(2); expect($queries[0]->sql)->toContain('pulse_entries'); expect($queries[1]->sql)->toContain('pulse_aggregates'); - expect($queries[0]->bindings)->toHaveCount(4 * 4); // 4 entries, 4 columns each - expect($queries[1]->bindings)->toHaveCount(2 * 6 * 4); // 2 entries, 6 columns each, 4 periods + if (DB::connection()->getDriverName() === 'sqlite') { + expect($queries[0]->bindings)->toHaveCount(4 * 5); // 4 entries, 5 columns each + expect($queries[1]->bindings)->toHaveCount(2 * 7 * 4); // 2 entries, 7 columns each, 4 periods + } else { + expect($queries[0]->bindings)->toHaveCount(4 * 4); // 4 entries, 4 columns each + expect($queries[1]->bindings)->toHaveCount(2 * 6 * 4); // 2 entries, 6 columns each, 4 periods + } $aggregates = Pulse::ignore(fn () => DB::table('pulse_aggregates')->where('period', 60)->orderBy('key')->pluck('value', 'key')); expect($aggregates['key1'])->toEqual(300); @@ -201,8 +216,13 @@ expect($queries)->toHaveCount(2); expect($queries[0]->sql)->toContain('pulse_entries'); expect($queries[1]->sql)->toContain('pulse_aggregates'); - expect($queries[0]->bindings)->toHaveCount(4 * 4); // 4 entries, 4 columns each - expect($queries[1]->bindings)->toHaveCount(2 * 6 * 4); // 2 entries, 6 columns each, 4 periods + if (DB::connection()->getDriverName() === 'sqlite') { + expect($queries[0]->bindings)->toHaveCount(4 * 5); // 4 entries, 5 columns each + expect($queries[1]->bindings)->toHaveCount(2 * 7 * 4); // 2 entries, 7 columns each, 4 periods + } else { + expect($queries[0]->bindings)->toHaveCount(4 * 4); // 4 entries, 4 columns each + expect($queries[1]->bindings)->toHaveCount(2 * 6 * 4); // 2 entries, 6 columns each, 4 periods + } $aggregates = Pulse::ignore(fn () => DB::table('pulse_aggregates')->where('period', 60)->orderBy('key')->pluck('value', 'key')); expect($aggregates['key1'])->toEqual(600); @@ -223,8 +243,13 @@ expect($queries)->toHaveCount(2); expect($queries[0]->sql)->toContain('pulse_entries'); expect($queries[1]->sql)->toContain('pulse_aggregates'); - expect($queries[0]->bindings)->toHaveCount(4 * 4); // 4 entries, 4 columns each - expect($queries[1]->bindings)->toHaveCount(2 * 7 * 4); // 2 entries, 7 columns each, 4 periods + if (DB::connection()->getDriverName() === 'sqlite') { + expect($queries[0]->bindings)->toHaveCount(4 * 5); // 4 entries, 5 columns each + expect($queries[1]->bindings)->toHaveCount(2 * 8 * 4); // 2 entries, 8 columns each, 4 periods + } else { + expect($queries[0]->bindings)->toHaveCount(4 * 4); // 4 entries, 4 columns each + expect($queries[1]->bindings)->toHaveCount(2 * 7 * 4); // 2 entries, 7 columns each, 4 periods + } $aggregates = Pulse::ignore(fn () => DB::table('pulse_aggregates')->where('period', 60)->orderBy('key')->get())->keyBy('key'); expect($aggregates['key1']->value)->toEqual(200); diff --git a/tests/Pest.php b/tests/Pest.php index e3a5b3b3..0c018bf0 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -103,6 +103,7 @@ function keyHash(string $string): string return match (DB::connection()->getDriverName()) { 'mysql' => hex2bin(md5($string)), 'pgsql' => Uuid::fromString(md5($string)), + 'sqlite' => md5($string), }; } From ff2755020e11dafc71266ad5ac1d98a24dc2723a Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Wed, 20 Dec 2023 01:46:32 +1100 Subject: [PATCH 095/110] Introduce `null` logger for testing (#245) --- src/Ingests/NullIngest.php | 36 ++++++++++++++++++++++++++++++++++++ src/PulseServiceProvider.php | 2 ++ 2 files changed, 38 insertions(+) create mode 100644 src/Ingests/NullIngest.php diff --git a/src/Ingests/NullIngest.php b/src/Ingests/NullIngest.php new file mode 100644 index 00000000..5ad747f5 --- /dev/null +++ b/src/Ingests/NullIngest.php @@ -0,0 +1,36 @@ + $items + */ + public function ingest(Collection $items): void + { + // + } + + /** + * Digest the ingested items. + */ + public function digest(Storage $storage): int + { + return 0; + } + + /** + * Trim the ingest. + */ + public function trim(): void + { + // + } +} diff --git a/src/PulseServiceProvider.php b/src/PulseServiceProvider.php index eb27b032..9eee6612 100644 --- a/src/PulseServiceProvider.php +++ b/src/PulseServiceProvider.php @@ -18,6 +18,7 @@ use Illuminate\View\Factory as ViewFactory; use Laravel\Pulse\Contracts\Ingest; use Laravel\Pulse\Contracts\Storage; +use Laravel\Pulse\Ingests\NullIngest; use Laravel\Pulse\Ingests\RedisIngest; use Laravel\Pulse\Ingests\StorageIngest; use Laravel\Pulse\Storage\DatabaseStorage; @@ -52,6 +53,7 @@ protected function registerIngest(): void $this->app->bind(Ingest::class, fn (Application $app) => match ($app->make('config')->get('pulse.ingest.driver')) { 'storage' => $app->make(StorageIngest::class), 'redis' => $app->make(RedisIngest::class), + null, 'null' => $app->make(NullIngest::class), default => throw new RuntimeException("Unknown ingest driver [{$app->make('config')->get('pulse.ingest.driver')}]."), }); } From 2aeb09f9e63a75b5954cbc42380cc0ecdb4b2143 Mon Sep 17 00:00:00 2001 From: Dries Vints Date: Tue, 19 Dec 2023 16:32:52 +0100 Subject: [PATCH 096/110] Rename UPGRADE.MD to UPGRADE.md --- UPGRADE.MD => UPGRADE.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename UPGRADE.MD => UPGRADE.md (100%) diff --git a/UPGRADE.MD b/UPGRADE.md similarity index 100% rename from UPGRADE.MD rename to UPGRADE.md From 440537c5956cd2ee1de724e34df05c251465ab42 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Wed, 20 Dec 2023 22:29:43 +0800 Subject: [PATCH 097/110] Preview pulse using sqlite by default (#248) Signed-off-by: Mior Muhammad Zaki --- testbench.yaml | 5 +++-- workbench/.env.example | 13 +++++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/testbench.yaml b/testbench.yaml index 87f67954..04839d92 100644 --- a/testbench.yaml +++ b/testbench.yaml @@ -5,9 +5,10 @@ migrations: - database/migrations workbench: - start: '/' + start: '/pulse' install: true - welcome: true + welcome: false build: - asset-publish + - create-sqlite-db - migrate-refresh diff --git a/workbench/.env.example b/workbench/.env.example index dd408896..613e197a 100644 --- a/workbench/.env.example +++ b/workbench/.env.example @@ -8,12 +8,13 @@ LOG_CHANNEL=stack LOG_DEPRECATIONS_CHANNEL=null LOG_LEVEL=debug -DB_CONNECTION=mysql -DB_HOST=127.0.0.1 -DB_PORT=3306 -DB_DATABASE=laravel_pulse -DB_USERNAME=root -DB_PASSWORD= +DB_CONNECTION=sqlite +# DB_CONNECTION=mysql +# DB_HOST=127.0.0.1 +# DB_PORT=3306 +# DB_DATABASE=laravel_pulse +# DB_USERNAME=root +# DB_PASSWORD= BROADCAST_DRIVER=log CACHE_DRIVER=file From a747c3297110da90fa7271690fe2f5d127a3ecb4 Mon Sep 17 00:00:00 2001 From: Jess Archer Date: Thu, 21 Dec 2023 11:42:40 +1000 Subject: [PATCH 098/110] [1.x] Allow complete control of user resolution (#251) * Allow complete control of user resolution * Add deprecated tag Co-authored-by: Tim MacDonald * Update ResolvesUsers.php * Update LegacyUsers.php * Update Users.php * Update Users.php --------- Co-authored-by: Tim MacDonald Co-authored-by: Taylor Otwell --- resources/views/livewire/usage.blade.php | 2 +- src/Contracts/ResolvesUsers.php | 28 ++++++ src/Facades/Pulse.php | 5 +- src/LegacyUsers.php | 64 ++++++++++++ src/Livewire/Usage.php | 20 +--- src/Pulse.php | 121 +++++++++-------------- src/PulseServiceProvider.php | 2 + src/Users.php | 92 +++++++++++++++++ tests/Feature/Livewire/UsageTest.php | 7 +- tests/Feature/PulseTest.php | 105 ++++++++++++++++++++ tests/User.php | 3 + 11 files changed, 349 insertions(+), 100 deletions(-) create mode 100644 src/Contracts/ResolvesUsers.php create mode 100644 src/LegacyUsers.php create mode 100644 src/Users.php diff --git a/resources/views/livewire/usage.blade.php b/resources/views/livewire/usage.blade.php index e53b68f1..f97eda06 100644 --- a/resources/views/livewire/usage.blade.php +++ b/resources/views/livewire/usage.blade.php @@ -40,7 +40,7 @@ class="flex-1" @else
@foreach ($userRequestCounts as $userRequestCount) - + @if ($userRequestCount->user->avatar ?? false) diff --git a/src/Contracts/ResolvesUsers.php b/src/Contracts/ResolvesUsers.php new file mode 100644 index 00000000..710c181c --- /dev/null +++ b/src/Contracts/ResolvesUsers.php @@ -0,0 +1,28 @@ + $keys + */ + public function load(Collection $keys): self; + + /** + * Eager load the users with the given keys. + * + * @return array{name: string, extra?: string, avatar?: string} + */ + public function fields(int|string|null $key): array; +} diff --git a/src/Facades/Pulse.php b/src/Facades/Pulse.php index aa21047b..a36302e3 100644 --- a/src/Facades/Pulse.php +++ b/src/Facades/Pulse.php @@ -19,12 +19,11 @@ * @method static int digest() * @method static bool wantsIngesting() * @method static \Illuminate\Support\Collection recorders() - * @method static \Illuminate\Support\Collection resolveUsers(\Illuminate\Support\Collection $ids) + * @method static \Laravel\Pulse\Contracts\ResolvesUsers resolveUsers(\Illuminate\Support\Collection $ids) * @method static \Laravel\Pulse\Pulse users(callable $callback) + * @method static \Laravel\Pulse\Pulse user(callable $callback) * @method static callable authenticatedUserIdResolver() * @method static string|int|null resolveAuthenticatedUserId() - * @method static \Laravel\Pulse\Pulse resolveAuthenticatedUserIdUsing(callable $callback) - * @method static mixed withUser(\Illuminate\Contracts\Auth\Authenticatable|string|int|null $user, callable $callback) * @method static \Laravel\Pulse\Pulse rememberUser(\Illuminate\Contracts\Auth\Authenticatable $user) * @method static \Laravel\Pulse\Pulse|string css(string|\Illuminate\Contracts\Support\Htmlable|array|null $css = null) * @method static string js() diff --git a/src/LegacyUsers.php b/src/LegacyUsers.php new file mode 100644 index 00000000..71db90fe --- /dev/null +++ b/src/LegacyUsers.php @@ -0,0 +1,64 @@ + + */ + protected Collection $resolvedUsers; + + /** + * @param callable $callback + */ + public function __construct(protected $callback) + { + // + } + + /** + * Return a unique key identifying the user. + */ + public function key(Authenticatable $user): int|string|null + { + return $user->getAuthIdentifier(); + } + + /** + * Eager load the users with the given keys. + * + * @param Collection $keys + */ + public function load(Collection $keys): self + { + $this->resolvedUsers = ($this->callback)($keys); + + return $this; + } + + /** + * Resolve the user fields for the user with the given key. + * + * @return array{name: string, extra?: string, avatar?: string} + */ + public function fields(int|string|null $key): array + { + $user = $this->resolvedUsers->firstWhere('id', $key); + + return [ + 'name' => $user['name'] ?? "ID: $key", + 'extra' => $user['extra'] ?? $user['email'] ?? '', + 'avatar' => $user['avatar'] ?? (($user['email'] ?? false) + ? sprintf('https://gravatar.com/avatar/%s?d=mp', hash('sha256', trim(strtolower($user['email'])))) + : sprintf('https://gravatar.com/avatar?d=mp') + ), + ]; + } +} diff --git a/src/Livewire/Usage.php b/src/Livewire/Usage.php index 9a99a4ad..4c517002 100644 --- a/src/Livewire/Usage.php +++ b/src/Livewire/Usage.php @@ -54,21 +54,11 @@ function () use ($type) { $users = Pulse::resolveUsers($counts->pluck('key')); - return $counts->map(function ($row) use ($users) { - $user = $users->firstWhere('id', $row->key); - - return (object) [ - 'user' => (object) [ - 'id' => $row->key, - 'name' => $user['name'] ?? 'Unknown', - 'extra' => $user['extra'] ?? $user['email'] ?? '', - 'avatar' => $user['avatar'] ?? (($user['email'] ?? false) - ? sprintf('https://gravatar.com/avatar/%s?d=mp', hash('sha256', trim(strtolower($user['email'])))) - : null), - ], - 'count' => (int) $row->count, - ]; - }); + return $counts->map(fn ($row) => (object) [ + 'key' => $row->key, + 'user' => (object) $users->fields($row->key), + 'count' => (int) $row->count, + ]); }, $type ); diff --git a/src/Pulse.php b/src/Pulse.php index 168869a0..f3e979f8 100644 --- a/src/Pulse.php +++ b/src/Pulse.php @@ -14,6 +14,7 @@ use Illuminate\Support\Str; use Illuminate\Support\Traits\ForwardsCalls; use Laravel\Pulse\Contracts\Ingest; +use Laravel\Pulse\Contracts\ResolvesUsers; use Laravel\Pulse\Contracts\Storage; use Laravel\Pulse\Events\ExceptionReported; use RuntimeException; @@ -61,20 +62,6 @@ class Pulse */ protected Collection $filters; - /** - * The users resolver. - * - * @var ?callable(\Illuminate\Support\Collection): iterable - */ - protected $usersResolver = null; - - /** - * The authenticated user ID resolver. - * - * @var callable(): (int|string|null) - */ - protected $authenticatedUserIdResolver = null; - /** * The remembered user's ID. */ @@ -361,37 +348,43 @@ public function recorders(): Collection /** * Resolve the user details for the given user IDs. * - * @param \Illuminate\Support\Collection $ids - * @return \Illuminate\Support\Collection + * @param \Illuminate\Support\Collection $keys */ - public function resolveUsers(Collection $ids): Collection + public function resolveUsers(Collection $keys): ResolvesUsers { - if ($this->usersResolver) { - return collect(($this->usersResolver)($ids)); - } + $resolver = $this->app->make(ResolvesUsers::class); - if (class_exists($class = \App\Models\User::class) || class_exists($class = \App\User::class)) { - return $class::whereKey($ids)->get()->map(fn ($user) => [ - 'id' => $user->getKey(), - 'name' => $user->name, - 'email' => $user->email, - ]); - } + return $resolver->load($keys); + } - return $ids->map(fn (string|int $id) => [ - 'id' => $id, - 'name' => "User ID: {$id}", - ]); + /** + * Resolve the users' details using the given closure. + * + * @deprecated + * + * @param callable(\Illuminate\Support\Collection): ?iterable $callback + */ + public function users(callable $callback): self + { + $this->app->instance(ResolvesUsers::class, new LegacyUsers($callback)); + + return $this; } /** * Resolve the user's details using the given closure. * - * @param (callable(\Illuminate\Support\Collection): iterable) $callback + * @param callable(\Illuminate\Contracts\Auth\Authenticatable): array{name: string, email?: ?string, avatar?: ?string, extra?: ?string} $callback */ - public function users(callable $callback): self + public function user(callable $callback): self { - $this->usersResolver = $callback; + $resolver = $this->app->make(ResolvesUsers::class); + + if (! method_exists($resolver, 'setFieldResolver')) { + throw new RuntimeException('The configured user resolver does not support setting user fields'); + } + + $resolver->setFieldResolver($callback); // @phpstan-ignore method.nonObject return $this; } @@ -403,19 +396,26 @@ public function users(callable $callback): self */ public function authenticatedUserIdResolver(): callable { - if ($this->authenticatedUserIdResolver !== null) { - return $this->authenticatedUserIdResolver; - } - $auth = $this->app->make('auth'); if ($auth->hasUser()) { - $id = $auth->id(); + $resolver = $this->app->make(ResolvesUsers::class); + $key = $resolver->key($auth->user()); - return fn () => $id; + return fn () => $key; } - return fn () => $auth->id() ?? $this->rememberedUserId; + return function () { + $auth = $this->app->make('auth'); + + if ($auth->hasUser()) { + $resolver = $this->app->make(ResolvesUsers::class); + + return $resolver->key($auth->user()); + } else { + return $this->rememberedUserId; + } + }; } /** @@ -426,47 +426,14 @@ public function resolveAuthenticatedUserId(): string|int|null return $this->authenticatedUserIdResolver()(); } - /** - * Resolve the authenticated user ID with the given callback. - */ - public function resolveAuthenticatedUserIdUsing(callable $callback): self - { - $this->authenticatedUserIdResolver = $callback; - - return $this; - } - - /** - * Set the user for the given callback. - * - * @template TReturn - * - * @param (callable(): TReturn) $callback - * @return TReturn - */ - public function withUser(Authenticatable|int|string|null $user, callable $callback): mixed - { - $cachedUserIdResolver = $this->authenticatedUserIdResolver; - - try { - $id = $user instanceof Authenticatable - ? $user->getAuthIdentifier() - : $user; - - $this->authenticatedUserIdResolver = fn () => $id; - - return $callback(); - } finally { - $this->authenticatedUserIdResolver = $cachedUserIdResolver; - } - } - /** * Remember the authenticated user's ID. */ public function rememberUser(Authenticatable $user): self { - $this->rememberedUserId = $user->getAuthIdentifier(); + $resolver = $this->app->make(ResolvesUsers::class); + + $this->rememberedUserId = $resolver->key($user); return $this; } diff --git a/src/PulseServiceProvider.php b/src/PulseServiceProvider.php index 9eee6612..51974577 100644 --- a/src/PulseServiceProvider.php +++ b/src/PulseServiceProvider.php @@ -17,6 +17,7 @@ use Illuminate\View\Compilers\BladeCompiler; use Illuminate\View\Factory as ViewFactory; use Laravel\Pulse\Contracts\Ingest; +use Laravel\Pulse\Contracts\ResolvesUsers; use Laravel\Pulse\Contracts\Storage; use Laravel\Pulse\Ingests\NullIngest; use Laravel\Pulse\Ingests\RedisIngest; @@ -41,6 +42,7 @@ public function register(): void $this->app->singleton(Pulse::class); $this->app->bind(Storage::class, DatabaseStorage::class); + $this->app->singletonIf(ResolvesUsers::class, Users::class); $this->registerIngest(); } diff --git a/src/Users.php b/src/Users.php new file mode 100644 index 00000000..837ed7ce --- /dev/null +++ b/src/Users.php @@ -0,0 +1,92 @@ + + */ + protected Collection $resolvedUsers; + + /** + * The field resolver. + * + * @var ?callable(\Illuminate\Contracts\Auth\Authenticatable): array{name: string, extra?: string, avatar?: string} + */ + protected $fieldResolver = null; + + /** + * Return a unique key identifying the user. + */ + public function key(Authenticatable $user): int|string|null + { + return $user->getAuthIdentifier(); + } + + /** + * Eager load the users with the given keys. + * + * @param Collection $keys + */ + public function load(Collection $keys): self + { + $auth = app('auth'); + + $provider = $auth->createUserProvider( + config("auth.guards.{$auth->getDefaultDriver()}.provider") + ); + + if ($provider instanceof EloquentUserProvider) { + $model = $provider->getModel(); + + $this->resolvedUsers = $model::findMany($keys); + } else { + $this->resolvedUsers = $keys->map(fn ($key) => $provider->retrieveById($key)); + } + + return $this; + } + + /** + * Resolve the user fields for the user with the given key. + * + * @return array{name: string, extra?: string, avatar?: string} + */ + public function fields(int|string|null $key): array + { + $user = $this->resolvedUsers->first(fn ($user) => $user->getAuthIdentifier() == $key); + + if ($this->fieldResolver !== null && $user !== null) { + return ($this->fieldResolver)($user); + } + + return [ + 'name' => $user->name ?? "ID: $key", + 'extra' => $user->email ?? '', + 'avatar' => $user->avatar ?? (($user->email ?? false) + ? sprintf('https://gravatar.com/avatar/%s?d=mp', hash('sha256', trim(strtolower($user->email)))) // @phpstan-ignore property.nonObject + : sprintf('https://gravatar.com/avatar?d=mp') + ), + ]; + } + + /** + * Override the field resolver. + * + * @param callable(\Illuminate\Contracts\Auth\Authenticatable): array{name: string, extra?: string, avatar?: string} $resolver + */ + public function setFieldResolver(callable $resolver): self + { + $this->fieldResolver = $resolver; + + return $this; + } +} diff --git a/tests/Feature/Livewire/UsageTest.php b/tests/Feature/Livewire/UsageTest.php index c8526eb1..fba265a1 100644 --- a/tests/Feature/Livewire/UsageTest.php +++ b/tests/Feature/Livewire/UsageTest.php @@ -16,7 +16,6 @@ it('renders top 10 users making requests', function (string $query, string $type) { $users = User::factory(3)->create(); - Pulse::users(fn () => $users); // Add entries outside of the window. Carbon::setTestNow('2000-01-01 12:00:00'); @@ -47,9 +46,9 @@ Livewire::withQueryParams(['usage' => $query]) ->test(Usage::class, ['lazy' => false]) ->assertViewHas('userRequestCounts', collect([ - (object) ['count' => 6, 'user' => (object) ['id' => (string) $users[0]->id, 'name' => $users[0]->name, 'extra' => $users[0]->email, 'avatar' => avatar($users[0]->email)]], - (object) ['count' => 4, 'user' => (object) ['id' => (string) $users[1]->id, 'name' => $users[1]->name, 'extra' => $users[1]->email, 'avatar' => avatar($users[1]->email)]], - (object) ['count' => 2, 'user' => (object) ['id' => (string) $users[2]->id, 'name' => $users[2]->name, 'extra' => $users[2]->email, 'avatar' => avatar($users[2]->email)]], + (object) ['key' => $users[0]->id, 'count' => 6, 'user' => (object) ['name' => $users[0]->name, 'extra' => $users[0]->email, 'avatar' => avatar($users[0]->email)]], + (object) ['key' => $users[1]->id, 'count' => 4, 'user' => (object) ['name' => $users[1]->name, 'extra' => $users[1]->email, 'avatar' => avatar($users[1]->email)]], + (object) ['key' => $users[2]->id, 'count' => 2, 'user' => (object) ['name' => $users[2]->name, 'extra' => $users[2]->email, 'avatar' => avatar($users[2]->email)]], ])); })->with([ ['requests', 'user_request'], diff --git a/tests/Feature/PulseTest.php b/tests/Feature/PulseTest.php index 3e9d8464..425f8625 100644 --- a/tests/Feature/PulseTest.php +++ b/tests/Feature/PulseTest.php @@ -1,11 +1,16 @@ toBe(0); }); + +it('resolves the authenticated user ID', function () { + Auth::login(User::factory()->make(['id' => 123])); + + expect(Pulse::resolveAuthenticatedUserId())->toBe(123); +}); + +it('resolves users', function () { + Pulse::stopRecording(); + User::factory()->create(['id' => 123, 'name' => 'Jess Archer', 'email' => 'jess@example.com']); + User::factory()->create(['id' => 456, 'name' => 'Tim MacDonald', 'email' => 'tim@example.com']); + + $resolved = Pulse::resolveUsers(collect([123, 456])); + + expect($resolved->fields(123))->toBe([ + 'name' => 'Jess Archer', + 'extra' => 'jess@example.com', + 'avatar' => 'https://gravatar.com/avatar/d72141e224a6aa94fbd060f142e82aaadc05b1fed044017c230ab882523e2673?d=mp', + ]); + expect($resolved->fields(456))->toBe([ + 'name' => 'Tim MacDonald', + 'extra' => 'tim@example.com', + 'avatar' => 'https://gravatar.com/avatar/8f3b8f8fc3a3ffd7e7d42e0749da576587e4a3a5409c6416439099eeb5b8b67c?d=mp', + ]); +}); + +it('can customize the user fields', function () { + Pulse::stopRecording(); + User::factory()->create(['id' => 123, 'name' => 'Jess Archer', 'email' => 'jess@jessarcher.com']); + + Pulse::user(fn ($user) => [ + 'name' => strtoupper($user->name), + 'extra' => strlen($user->name), + 'avatar' => 'https://example.com/avatar.png', + ]); + + $user = Pulse::resolveUsers(collect([123]))->fields(123); + + expect($user)->toBe([ + 'name' => 'JESS ARCHER', + 'extra' => 11, + 'avatar' => 'https://example.com/avatar.png', + ]); +}); + +it('maintains the users method for backwards compatibility', function () { + Pulse::stopRecording(); + User::factory()->create(['id' => 123, 'name' => 'Jess Archer', 'email' => 'jess@jessarcher.com']); + + Pulse::users(function ($ids) { + return User::findMany($ids)->map(fn ($user) => [ + 'id' => $user->id, + 'name' => strtolower($user->name), + 'extra' => strlen($user->name), + 'avatar' => 'https://example.com/avatar.png', + ]); + }); + + $user = Pulse::resolveUsers(collect([123]))->fields(123); + + expect($user)->toBe([ + 'name' => 'jess archer', + 'extra' => 11, + 'avatar' => 'https://example.com/avatar.png', + ]); +}); + +it('can customize user resolving', function () { + app()->singleton(ResolvesUsers::class, fn () => new class implements ResolvesUsers + { + public function key(Authenticatable $user): int|string|null + { + return json_encode(['123', '456']); + } + + public function load(Collection $keys): self + { + return $this; + } + + public function fields(int|string|null $key): array + { + return [ + 'name' => 'Foo', + 'extra' => $key, + ]; + } + }); + + Auth::login(User::factory()->make(['id' => 123])); + + expect(Pulse::resolveAuthenticatedUserId())->toBe('["123","456"]'); + + $users = Pulse::resolveUsers(collect(['["123","456"]'])); + $user = $users->fields('["123","456"]'); + expect($user)->toBe([ + 'name' => 'Foo', + 'extra' => '["123","456"]', + ]); +}); diff --git a/tests/User.php b/tests/User.php index a4f000b0..c15e97f3 100644 --- a/tests/User.php +++ b/tests/User.php @@ -5,6 +5,7 @@ use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as AuthUser; +use Illuminate\Support\Facades\Hash; class User extends AuthUser { @@ -20,6 +21,8 @@ public function definition() { return [ 'name' => $this->faker->name(), + 'email' => $this->faker->safeEmail(), + 'password' => Hash::make('password'), ]; } }; From b0f368bb64585f384166b26f7cff3b228530cf78 Mon Sep 17 00:00:00 2001 From: taylorotwell Date: Thu, 21 Dec 2023 01:43:05 +0000 Subject: [PATCH 099/110] Update facade docblocks --- src/Facades/Pulse.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Facades/Pulse.php b/src/Facades/Pulse.php index a36302e3..9f513152 100644 --- a/src/Facades/Pulse.php +++ b/src/Facades/Pulse.php @@ -19,8 +19,7 @@ * @method static int digest() * @method static bool wantsIngesting() * @method static \Illuminate\Support\Collection recorders() - * @method static \Laravel\Pulse\Contracts\ResolvesUsers resolveUsers(\Illuminate\Support\Collection $ids) - * @method static \Laravel\Pulse\Pulse users(callable $callback) + * @method static \Laravel\Pulse\Contracts\ResolvesUsers resolveUsers(\Illuminate\Support\Collection $keys) * @method static \Laravel\Pulse\Pulse user(callable $callback) * @method static callable authenticatedUserIdResolver() * @method static string|int|null resolveAuthenticatedUserId() From a24b5ae40c9c6514ee0724e16cc75aa659e525f3 Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Thu, 21 Dec 2023 12:49:17 +1100 Subject: [PATCH 100/110] [1.x] Introduce buffer size (#250) * Introduce buffer size * Fix code styling * Static analysis * Collapse values * Fix code styling * postpone filtering until ingesting * Types * Make buffer env controlled * Update DatabaseStorage.php * Rename variable * Fix code styling --------- Co-authored-by: timacdonald Co-authored-by: Taylor Otwell --- config/pulse.php | 2 + src/Pulse.php | 53 ++++++++++++++++- src/Storage/DatabaseStorage.php | 16 +++++- tests/Feature/PulseTest.php | 57 +++++++++++++++++++ tests/Feature/Storage/DatabaseStorageTest.php | 20 +++++++ 5 files changed, 145 insertions(+), 3 deletions(-) diff --git a/config/pulse.php b/config/pulse.php index 28543ed9..c9fa055f 100644 --- a/config/pulse.php +++ b/config/pulse.php @@ -79,6 +79,8 @@ 'ingest' => [ 'driver' => env('PULSE_INGEST_DRIVER', 'storage'), + 'buffer' => env('PULSE_INGEST_BUFFER', 5_000), + 'trim' => [ 'lottery' => [1, 1_000], 'keep' => '7 days', diff --git a/src/Pulse.php b/src/Pulse.php index f3e979f8..ade8fd76 100644 --- a/src/Pulse.php +++ b/src/Pulse.php @@ -86,6 +86,11 @@ class Pulse */ protected $css = [__DIR__.'/../dist/pulse.css']; + /** + * Indicates that Pulse is currently evaluating the buffer. + */ + protected bool $evaluatingBuffer = false; + /** * Create a new Pulse instance. */ @@ -153,6 +158,8 @@ public function record( if ($this->shouldRecord) { $this->entries[] = $entry; + + $this->ingestWhenOverBufferSize(); } return $entry; @@ -178,6 +185,8 @@ public function set( if ($this->shouldRecord) { $this->entries[] = $value; + + $this->ingestWhenOverBufferSize(); } return $value; @@ -190,6 +199,8 @@ public function lazy(callable $closure): self { if ($this->shouldRecord) { $this->lazy[] = $closure; + + $this->ingestWhenOverBufferSize(); } return $this; @@ -277,7 +288,7 @@ public function filter(callable $filter): self */ public function ingest(): int { - $this->rescue(fn () => $this->lazy->each(fn ($lazy) => $lazy())); + $this->resolveLazyEntries(); return $this->ignore(function () { $entries = $this->rescue(fn () => $this->entries->filter($this->shouldRecord(...))) ?? collect([]); @@ -327,6 +338,46 @@ public function wantsIngesting(): bool return $this->lazy->isNotEmpty() || $this->entries->isNotEmpty(); } + /** + * Start ingesting entires if over buffer size. + */ + protected function ingestWhenOverBufferSize(): void + { + // To prevent recursion, we track when we are already evaluating the + // buffer and resolving entries. When we are we may simply return + // and the continue execution. We set the value to false later. + if ($this->evaluatingBuffer) { + return; + } + + // TODO remove fallback when tagging v1 + $buffer = $this->app->make('config')->get('pulse.ingest.buffer') ?? 5_000; + + if (($this->entries->count() + $this->lazy->count()) > $buffer) { + $this->evaluatingBuffer = true; + + $this->resolveLazyEntries(); + } + + if ($this->entries->count() > $buffer) { + $this->evaluatingBuffer = true; + + $this->ingest(); + } + + $this->evaluatingBuffer = false; + } + + /** + * Resolve lazy entries. + */ + protected function resolveLazyEntries(): void + { + $this->rescue(fn () => $this->lazy->each(fn ($lazy) => $lazy())); + + $this->lazy = collect([]); + } + /** * Determine if the given entry should be recorded. */ diff --git a/src/Storage/DatabaseStorage.php b/src/Storage/DatabaseStorage.php index 4456f082..258c7485 100644 --- a/src/Storage/DatabaseStorage.php +++ b/src/Storage/DatabaseStorage.php @@ -98,7 +98,8 @@ public function store(Collection $items): void ->chunk($this->config->get('pulse.storage.database.chunk')) ->each(fn ($chunk) => $this->upsertAvg($chunk->all())); - $values + $this + ->collapseValues($values) ->chunk($this->config->get('pulse.storage.database.chunk')) ->each(fn ($chunk) => $this->connection() ->table('pulse_values') @@ -108,7 +109,7 @@ public function store(Collection $items): void ...($attributes = $entry->attributes()), 'key_hash' => md5($attributes['key']), ])->all() - : $chunk->map->attributes()->all(), + : $chunk->map->attributes()->all(), // @phpstan-ignore method.notFound ['type', 'key_hash'], ['timestamp', 'value'] ) @@ -351,6 +352,17 @@ protected function preaggregateAverages(Collection $entries): Collection ]); } + /** + * Collapse the given values. + * + * @param \Illuminate\Support\Collection $values + * @return \Illuminate\Support\Collection + */ + protected function collapseValues(Collection $values): Collection + { + return $values->reverse()->unique(fn (Value $value) => [$value->key, $value->type]); + } + /** * Pre-aggregate entries with a callback. * diff --git a/tests/Feature/PulseTest.php b/tests/Feature/PulseTest.php index 425f8625..1fd9f8e1 100644 --- a/tests/Feature/PulseTest.php +++ b/tests/Feature/PulseTest.php @@ -4,6 +4,7 @@ use Illuminate\Support\Collection; use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Config; use Laravel\Pulse\Contracts\ResolvesUsers; use Laravel\Pulse\Contracts\Storage; use Laravel\Pulse\Entry; @@ -164,3 +165,59 @@ public function fields(int|string|null $key): array 'extra' => '["123","456"]', ]); }); + +it('can limit the buffer size of entries', function () { + Config::set('pulse.ingest.buffer', 4); + + Pulse::record('type', 'key'); + expect(Pulse::wantsIngesting())->toBeTrue(); + Pulse::record('type', 'key'); + expect(Pulse::wantsIngesting())->toBeTrue(); + Pulse::record('type', 'key'); + expect(Pulse::wantsIngesting())->toBeTrue(); + Pulse::record('type', 'key'); + expect(Pulse::wantsIngesting())->toBeTrue(); + Pulse::record('type', 'key'); + expect(Pulse::wantsIngesting())->toBeFalse(); + + Pulse::set('type', 'key', 'value'); + expect(Pulse::wantsIngesting())->toBeTrue(); + Pulse::set('type', 'key', 'value'); + expect(Pulse::wantsIngesting())->toBeTrue(); + Pulse::set('type', 'key', 'value'); + expect(Pulse::wantsIngesting())->toBeTrue(); + Pulse::set('type', 'key', 'value'); + expect(Pulse::wantsIngesting())->toBeTrue(); + Pulse::set('type', 'key', 'value'); + expect(Pulse::wantsIngesting())->toBeFalse(); +}); + +it('resolves lazy entries when considering the buffer', function () { + Config::set('pulse.ingest.buffer', 4); + + Pulse::lazy(fn () => Pulse::record('type', 'key')); + expect(Pulse::wantsIngesting())->toBeTrue(); + Pulse::lazy(fn () => Pulse::set('type', 'key', 'value')); + expect(Pulse::wantsIngesting())->toBeTrue(); + Pulse::lazy(fn () => Pulse::record('type', 'key')); + expect(Pulse::wantsIngesting())->toBeTrue(); + Pulse::lazy(fn () => Pulse::set('type', 'key', 'value')); + expect(Pulse::wantsIngesting())->toBeTrue(); + Pulse::lazy(fn () => Pulse::record('type', 'key')); + expect(Pulse::wantsIngesting())->toBeFalse(); +}); + +it('rescues exceptions that occur while filtering', function () { + $handled = false; + Pulse::handleExceptionsUsing(function () use (&$handled) { + $handled = true; + }); + + Pulse::filter(function ($entry) { + throw new RuntimeException('Whoops!'); + }); + Pulse::record('type', 'key'); + Pulse::ingest(); + + expect($handled)->toBe(true); +}); diff --git a/tests/Feature/Storage/DatabaseStorageTest.php b/tests/Feature/Storage/DatabaseStorageTest.php index 0a9b1d48..184cb320 100644 --- a/tests/Feature/Storage/DatabaseStorageTest.php +++ b/tests/Feature/Storage/DatabaseStorageTest.php @@ -1,6 +1,7 @@ 6, ]); }); + +it('collapses values with the same key into a single upsert', function () { + $bindings = []; + DB::listen(function (QueryExecuted $event) use (&$bindings) { + $bindings = $event->bindings; + }); + + Pulse::set('read_counter', 'post:321', 123); + Pulse::set('read_counter', 'post:321', 234); + Pulse::set('read_counter', 'post:321', 345); + Pulse::ingest(); + + expect($bindings)->not->toContain(123); + expect($bindings)->not->toContain(234); + expect($bindings)->toContain('345'); + $values = Pulse::ignore(fn () => DB::table('pulse_values')->get()); + expect($values)->toHaveCount(1); + expect($values[0]->value)->toBe('345'); +}); From e5dd81025eb7f2add691b8deef492ab5e84ecdbc Mon Sep 17 00:00:00 2001 From: Jess Archer Date: Sat, 23 Dec 2023 00:34:54 +1000 Subject: [PATCH 101/110] Make `aggregateTotal` return a float when only a single type is requested (#259) --- src/Contracts/Storage.php | 4 +- src/Storage/DatabaseStorage.php | 27 ++++++++----- tests/Feature/Storage/DatabaseStorageTest.php | 39 ++++++++++++++++++- 3 files changed, 58 insertions(+), 12 deletions(-) diff --git a/src/Contracts/Storage.php b/src/Contracts/Storage.php index 980a0c70..40b81f49 100644 --- a/src/Contracts/Storage.php +++ b/src/Contracts/Storage.php @@ -90,11 +90,11 @@ public function aggregateTypes( * * @param string|list $types * @param 'count'|'min'|'max'|'sum'|'avg' $aggregate - * @return \Illuminate\Support\Collection + * @return float|\Illuminate\Support\Collection */ public function aggregateTotal( array|string $types, string $aggregate, CarbonInterval $interval, - ): Collection; + ): float|Collection; } diff --git a/src/Storage/DatabaseStorage.php b/src/Storage/DatabaseStorage.php index 258c7485..46133f40 100644 --- a/src/Storage/DatabaseStorage.php +++ b/src/Storage/DatabaseStorage.php @@ -709,19 +709,17 @@ public function aggregateTypes( * * @param string|list $types * @param 'count'|'min'|'max'|'sum'|'avg' $aggregate - * @return \Illuminate\Support\Collection + * @return float|\Illuminate\Support\Collection */ public function aggregateTotal( array|string $types, string $aggregate, CarbonInterval $interval, - ): Collection { + ): float|Collection { if (! in_array($aggregate, $allowed = ['count', 'min', 'max', 'sum', 'avg'])) { throw new InvalidArgumentException("Invalid aggregate type [$aggregate], allowed types: [".implode(', ', $allowed).'].'); } - $types = is_array($types) ? $types : [$types]; - $now = CarbonImmutable::now(); $period = $interval->totalSeconds / 60; $windowStart = (int) ($now->getTimestamp() - $interval->totalSeconds + 1); @@ -731,7 +729,7 @@ public function aggregateTotal( $tailEnd = $oldestBucket - 1; return $this->connection()->query() - ->addSelect('type') + ->when(is_array($types), fn ($query) => $query->addSelect('type')) ->selectRaw(match ($aggregate) { 'count' => "sum({$this->wrap('count')})", 'min' => "min({$this->wrap('min')})", @@ -750,7 +748,11 @@ public function aggregateTotal( 'avg' => "avg({$this->wrap('value')})", }." as {$this->wrap($aggregate)}") ->from('pulse_entries') - ->whereIn('type', $types) + ->when( + is_array($types), + fn ($query) => $query->whereIn('type', $types), + fn ($query) => $query->where('type', $types) + ) ->where('timestamp', '>=', $tailStart) ->where('timestamp', '<=', $tailEnd) ->groupBy('type') @@ -766,15 +768,22 @@ public function aggregateTotal( }." as {$this->wrap($aggregate)}") ->from('pulse_aggregates') ->where('period', $period) - ->whereIn('type', $types) + ->when( + is_array($types), + fn ($query) => $query->whereIn('type', $types), + fn ($query) => $query->where('type', $types) + ) ->where('aggregate', $aggregate) ->where('bucket', '>=', $oldestBucket) ->groupBy('type') ), as: 'child' ) ->groupBy('type') - ->get() - ->pluck($aggregate, 'type'); + ->when( + is_array($types), + fn ($query) => $query->pluck($aggregate, 'type'), + fn ($query) => (float) $query->value($aggregate) + ); } /** diff --git a/tests/Feature/Storage/DatabaseStorageTest.php b/tests/Feature/Storage/DatabaseStorageTest.php index 184cb320..b04d04dc 100644 --- a/tests/Feature/Storage/DatabaseStorageTest.php +++ b/tests/Feature/Storage/DatabaseStorageTest.php @@ -372,7 +372,44 @@ ]); }); -test('one aggregate for multiple types, totals', function () { +test('total aggregate for a single type', function () { + // Add entries outside of the window + Carbon::setTestNow('2000-01-01 12:00:00'); + Pulse::record('cache_hit', 'flight:*')->count(); + Pulse::record('cache_hit', 'flight:*')->count(); + + // Add entries to the "tail" + Carbon::setTestNow('2000-01-01 12:00:01'); + Pulse::record('cache_hit', 'flight:*')->count(); + Pulse::record('cache_hit', 'flight:*')->count(); + Carbon::setTestNow('2000-01-01 12:00:02'); + Pulse::record('cache_hit', 'flight:*')->count(); + Pulse::record('cache_hit', 'flight:*')->count(); + Carbon::setTestNow('2000-01-01 12:00:03'); + Pulse::record('cache_hit', 'flight:*')->count(); + Pulse::record('cache_hit', 'flight:*')->count(); + + // Add entries to the current buckets. + Carbon::setTestNow('2000-01-01 12:59:00'); + Pulse::record('cache_hit', 'flight:*')->count(); + Pulse::record('cache_hit', 'flight:*')->count(); + Carbon::setTestNow('2000-01-01 12:59:10'); + Pulse::record('cache_hit', 'flight:*')->count(); + Pulse::record('cache_hit', 'flight:*')->count(); + Carbon::setTestNow('2000-01-01 12:59:20'); + Pulse::record('cache_hit', 'flight:*')->count(); + Pulse::record('cache_hit', 'flight:*')->count(); + + Pulse::ingest(); + + Carbon::setTestNow('2000-01-01 13:00:00'); + + $total = Pulse::aggregateTotal('cache_hit', 'count', CarbonInterval::hour()); + + expect($total)->toEqual(12); +}); + +test('total aggregate for multiple types', function () { /* | type | count | |------------|-------| From ff609d635f02c72c58e75f164a40805eb0d78a1b Mon Sep 17 00:00:00 2001 From: Jess Archer Date: Sat, 23 Dec 2023 00:35:10 +1000 Subject: [PATCH 102/110] Allow passing user object directly to user card (#258) --- .../views/components/user-card.blade.php | 12 ++++++---- resources/views/livewire/usage.blade.php | 24 +++++++------------ 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/resources/views/components/user-card.blade.php b/resources/views/components/user-card.blade.php index fcf9ddb6..81cbabe4 100644 --- a/resources/views/components/user-card.blade.php +++ b/resources/views/components/user-card.blade.php @@ -1,16 +1,20 @@ +@props(['user' => null, 'name' => null, 'extra' => null, 'avatar' => null, 'stats' => null]) +
merge(['class' => 'flex items-center justify-between p-3 gap-3 bg-gray-50 dark:bg-gray-800/50 rounded']) }}>
@if (isset($avatar)) {{ $avatar }} + @elseif ($user->avatar ?? false) + {{ $user->name }} @endif
-
- {{ $name }} +
+ {{ $user->name ?? $name }}
-
- {{ $extra }} +
+ {{ $user->extra ?? $extra }}
diff --git a/resources/views/livewire/usage.blade.php b/resources/views/livewire/usage.blade.php index f97eda06..49609063 100644 --- a/resources/views/livewire/usage.blade.php +++ b/resources/views/livewire/usage.blade.php @@ -39,23 +39,17 @@ class="flex-1" @else
- @foreach ($userRequestCounts as $userRequestCount) - - @if ($userRequestCount->user->avatar ?? false) - - - - @endif + @php + $sampleRate = match($this->usage) { + 'requests' => $userRequestsConfig['sample_rate'], + 'slow_requests' => $slowRequestsConfig['sample_rate'], + 'jobs' => $jobsConfig['sample_rate'], + }; + @endphp + @foreach ($userRequestCounts as $userRequestCount) + - @php - $sampleRate = match($this->usage) { - 'requests' => $userRequestsConfig['sample_rate'], - 'slow_requests' => $slowRequestsConfig['sample_rate'], - 'jobs' => $jobsConfig['sample_rate'], - }; - @endphp - @if ($sampleRate < 1) ~{{ number_format($userRequestCount->count * (1 / $sampleRate)) }} @else From 5c8a1c7a2e04bf7db77528a99b591295b496b7c9 Mon Sep 17 00:00:00 2001 From: taylorotwell Date: Fri, 22 Dec 2023 14:35:33 +0000 Subject: [PATCH 103/110] Update facade docblocks --- src/Facades/Pulse.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Facades/Pulse.php b/src/Facades/Pulse.php index 9f513152..1ab55093 100644 --- a/src/Facades/Pulse.php +++ b/src/Facades/Pulse.php @@ -40,7 +40,7 @@ * @method static \Illuminate\Support\Collection graph(array $types, string $aggregate, \Carbon\CarbonInterval $interval) * @method static \Illuminate\Support\Collection aggregate(string $type, string|array $aggregates, \Carbon\CarbonInterval $interval, string|null $orderBy = null, string $direction = 'desc', int $limit = 101) * @method static \Illuminate\Support\Collection aggregateTypes(string|array $types, string $aggregate, \Carbon\CarbonInterval $interval, string|null $orderBy = null, string $direction = 'desc', int $limit = 101) - * @method static \Illuminate\Support\Collection aggregateTotal(string|array $types, string $aggregate, \Carbon\CarbonInterval $interval) + * @method static float|\Illuminate\Support\Collection aggregateTotal(string|array $types, string $aggregate, \Carbon\CarbonInterval $interval) * * @see \Laravel\Pulse\Pulse */ From 38983d725e269ec41c56c2e4bda7640edd08264e Mon Sep 17 00:00:00 2001 From: Jess Archer Date: Sat, 23 Dec 2023 00:36:33 +1000 Subject: [PATCH 104/110] Rename `fields` method and return an object (#257) --- src/Contracts/ResolvesUsers.php | 6 +++--- src/LegacyUsers.php | 8 ++++---- src/Livewire/Usage.php | 2 +- src/Users.php | 14 +++++++------- tests/Feature/PulseTest.php | 20 ++++++++++---------- 5 files changed, 25 insertions(+), 25 deletions(-) diff --git a/src/Contracts/ResolvesUsers.php b/src/Contracts/ResolvesUsers.php index 710c181c..75fdeb6e 100644 --- a/src/Contracts/ResolvesUsers.php +++ b/src/Contracts/ResolvesUsers.php @@ -20,9 +20,9 @@ public function key(Authenticatable $user): int|string|null; public function load(Collection $keys): self; /** - * Eager load the users with the given keys. + * Find the user with the given key. * - * @return array{name: string, extra?: string, avatar?: string} + * @return object{name: string, extra?: string, avatar?: string} */ - public function fields(int|string|null $key): array; + public function find(int|string|null $key): object; } diff --git a/src/LegacyUsers.php b/src/LegacyUsers.php index 71db90fe..33a5fd1c 100644 --- a/src/LegacyUsers.php +++ b/src/LegacyUsers.php @@ -44,15 +44,15 @@ public function load(Collection $keys): self } /** - * Resolve the user fields for the user with the given key. + * Find the user with the given key. * - * @return array{name: string, extra?: string, avatar?: string} + * @return object{name: string, extra?: string, avatar?: string} */ - public function fields(int|string|null $key): array + public function find(int|string|null $key): object { $user = $this->resolvedUsers->firstWhere('id', $key); - return [ + return (object) [ 'name' => $user['name'] ?? "ID: $key", 'extra' => $user['extra'] ?? $user['email'] ?? '', 'avatar' => $user['avatar'] ?? (($user['email'] ?? false) diff --git a/src/Livewire/Usage.php b/src/Livewire/Usage.php index 4c517002..e66fbf09 100644 --- a/src/Livewire/Usage.php +++ b/src/Livewire/Usage.php @@ -56,7 +56,7 @@ function () use ($type) { return $counts->map(fn ($row) => (object) [ 'key' => $row->key, - 'user' => (object) $users->fields($row->key), + 'user' => $users->find($row->key), 'count' => (int) $row->count, ]); }, diff --git a/src/Users.php b/src/Users.php index 837ed7ce..d5550a6b 100644 --- a/src/Users.php +++ b/src/Users.php @@ -19,7 +19,7 @@ class Users implements ResolvesUsers /** * The field resolver. * - * @var ?callable(\Illuminate\Contracts\Auth\Authenticatable): array{name: string, extra?: string, avatar?: string} + * @var ?callable(\Illuminate\Contracts\Auth\Authenticatable): object{name: string, extra?: string, avatar?: string} */ protected $fieldResolver = null; @@ -56,19 +56,19 @@ public function load(Collection $keys): self } /** - * Resolve the user fields for the user with the given key. + * Find the user with the given key. * - * @return array{name: string, extra?: string, avatar?: string} + * @return object{name: string, extra?: string, avatar?: string} */ - public function fields(int|string|null $key): array + public function find(int|string|null $key): object { $user = $this->resolvedUsers->first(fn ($user) => $user->getAuthIdentifier() == $key); if ($this->fieldResolver !== null && $user !== null) { - return ($this->fieldResolver)($user); + return (object) ($this->fieldResolver)($user); } - return [ + return (object) [ 'name' => $user->name ?? "ID: $key", 'extra' => $user->email ?? '', 'avatar' => $user->avatar ?? (($user->email ?? false) @@ -81,7 +81,7 @@ public function fields(int|string|null $key): array /** * Override the field resolver. * - * @param callable(\Illuminate\Contracts\Auth\Authenticatable): array{name: string, extra?: string, avatar?: string} $resolver + * @param callable(\Illuminate\Contracts\Auth\Authenticatable): object{name: string, extra?: string, avatar?: string} $resolver */ public function setFieldResolver(callable $resolver): self { diff --git a/tests/Feature/PulseTest.php b/tests/Feature/PulseTest.php index 1fd9f8e1..0c4f0d81 100644 --- a/tests/Feature/PulseTest.php +++ b/tests/Feature/PulseTest.php @@ -79,12 +79,12 @@ $resolved = Pulse::resolveUsers(collect([123, 456])); - expect($resolved->fields(123))->toBe([ + expect($resolved->find(123))->toEqual((object) [ 'name' => 'Jess Archer', 'extra' => 'jess@example.com', 'avatar' => 'https://gravatar.com/avatar/d72141e224a6aa94fbd060f142e82aaadc05b1fed044017c230ab882523e2673?d=mp', ]); - expect($resolved->fields(456))->toBe([ + expect($resolved->find(456))->toEqual((object) [ 'name' => 'Tim MacDonald', 'extra' => 'tim@example.com', 'avatar' => 'https://gravatar.com/avatar/8f3b8f8fc3a3ffd7e7d42e0749da576587e4a3a5409c6416439099eeb5b8b67c?d=mp', @@ -101,9 +101,9 @@ 'avatar' => 'https://example.com/avatar.png', ]); - $user = Pulse::resolveUsers(collect([123]))->fields(123); + $user = Pulse::resolveUsers(collect([123]))->find(123); - expect($user)->toBe([ + expect($user)->toEqual((object) [ 'name' => 'JESS ARCHER', 'extra' => 11, 'avatar' => 'https://example.com/avatar.png', @@ -123,9 +123,9 @@ ]); }); - $user = Pulse::resolveUsers(collect([123]))->fields(123); + $user = Pulse::resolveUsers(collect([123]))->find(123); - expect($user)->toBe([ + expect($user)->toEqual((object) [ 'name' => 'jess archer', 'extra' => 11, 'avatar' => 'https://example.com/avatar.png', @@ -145,9 +145,9 @@ public function load(Collection $keys): self return $this; } - public function fields(int|string|null $key): array + public function find(int|string|null $key): object { - return [ + return (object) [ 'name' => 'Foo', 'extra' => $key, ]; @@ -159,8 +159,8 @@ public function fields(int|string|null $key): array expect(Pulse::resolveAuthenticatedUserId())->toBe('["123","456"]'); $users = Pulse::resolveUsers(collect(['["123","456"]'])); - $user = $users->fields('["123","456"]'); - expect($user)->toBe([ + $user = $users->find('["123","456"]'); + expect($user)->toEqual((object) [ 'name' => 'Foo', 'extra' => '["123","456"]', ]); From bf73282f71f394f73b0d351c2fd6c2a9cae55f8f Mon Sep 17 00:00:00 2001 From: Jess Archer Date: Wed, 27 Dec 2023 12:52:40 +1000 Subject: [PATCH 105/110] Fix helper return (#261) --- src/Livewire/Card.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Livewire/Card.php b/src/Livewire/Card.php index 0d719b39..8993274a 100644 --- a/src/Livewire/Card.php +++ b/src/Livewire/Card.php @@ -138,12 +138,12 @@ protected function aggregateTypes( * * @param string|list $types * @param 'count'|'min'|'max'|'sum'|'avg' $aggregate - * @return \Illuminate\Support\Collection + * @return float|\Illuminate\Support\Collection */ protected function aggregateTotal( array|string $types, string $aggregate, - ): Collection { + ): float|Collection { return Pulse::aggregateTotal($types, $aggregate, $this->periodAsInterval()); } } From 32fb030ebf9679a30c373cada2905568d3cf818e Mon Sep 17 00:00:00 2001 From: Jess Archer Date: Thu, 28 Dec 2023 00:15:20 +1000 Subject: [PATCH 106/110] [1.x] Prevent `pulse:check` from filling the file cache (#260) * Fix continually growing cache with file cache * Prevent shared beat from impacting isolated beat * Fix static analysis * Formatting Co-authored-by: Tim MacDonald --------- Co-authored-by: Tim MacDonald --- src/Commands/CheckCommand.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Commands/CheckCommand.php b/src/Commands/CheckCommand.php index 6905a28b..1eb5702e 100644 --- a/src/Commands/CheckCommand.php +++ b/src/Commands/CheckCommand.php @@ -48,6 +48,10 @@ public function handle( $lastSnapshotAt = CarbonImmutable::now()->floorSeconds((int) $interval->totalSeconds); + $lock = ($store = $cache->store()->getStore()) instanceof LockProvider + ? $store->lock('laravel:pulse:check', (int) $interval->totalSeconds) + : null; + while (true) { $now = CarbonImmutable::now(); @@ -63,15 +67,12 @@ public function handle( $lastSnapshotAt = $now->floorSeconds((int) $interval->totalSeconds); - $event->dispatch(new SharedBeat($lastSnapshotAt, $interval)); - - if ( - ($lockProvider ??= $cache->store()->getStore()) instanceof LockProvider && - $lockProvider->lock("laravel:pulse:check:{$lastSnapshotAt->getTimestamp()}", (int) $interval->totalSeconds)->get() - ) { + if ($lock?->get()) { $event->dispatch(new IsolatedBeat($lastSnapshotAt, $interval)); } + $event->dispatch(new SharedBeat($lastSnapshotAt, $interval)); + $pulse->ingest(); } } From a9c76cf365dcf16de411fcbc1af4dfd9cb520ae8 Mon Sep 17 00:00:00 2001 From: Jaanus Vapper Date: Sat, 6 Jan 2024 19:00:04 +0200 Subject: [PATCH 107/110] Use (potentially overloaded) key() method for finding user (#267) * Use (potentially overloaded) key() method for finding user * don't use strict compare for user identifier --- src/Users.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Users.php b/src/Users.php index d5550a6b..7de0b343 100644 --- a/src/Users.php +++ b/src/Users.php @@ -62,7 +62,7 @@ public function load(Collection $keys): self */ public function find(int|string|null $key): object { - $user = $this->resolvedUsers->first(fn ($user) => $user->getAuthIdentifier() == $key); + $user = $this->resolvedUsers->first(fn ($user) => $this->key($user) == $key); if ($this->fieldResolver !== null && $user !== null) { return (object) ($this->fieldResolver)($user); From 5ba83b9f5792c30d0671646e4f783149d32f8c32 Mon Sep 17 00:00:00 2001 From: Austin White Date: Sat, 6 Jan 2024 09:05:52 -0800 Subject: [PATCH 108/110] [1.x] Add mariadb test workflow (#264) * Add support for mariadb driver. * Added tests * Update tests.yml * Remove config changes * Remove matrix * Formatting --------- Co-authored-by: Tim MacDonald --- .github/workflows/tests.yml | 53 +++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b2bcbf15..7ab7f290 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -63,6 +63,59 @@ jobs: DB_CONNECTION: mysql DB_USERNAME: root + mariadb: + runs-on: ubuntu-22.04 + + services: + mysql: + image: mariadb:10.5 + env: + MYSQL_ALLOW_EMPTY_PASSWORD: yes + MYSQL_DATABASE: forge + ports: + - 3306:3306 + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + redis: + image: redis + ports: + - 6379:6379 + options: --entrypoint redis-server + + strategy: + fail-fast: true + matrix: + php: [8.3] + laravel: [10] + stability: [prefer-stable] + + name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} - Stability ${{ matrix.stability }} - MariaDB 10.5 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: dom, curl, libxml, mbstring, redis, pcntl, zip + ini-values: error_reporting=E_ALL + tools: composer:v2 + coverage: none + + - name: Install redis-cli + run: sudo apt-get install -qq redis-tools + + - name: Install dependencies + run: | + composer update --prefer-dist --no-interaction --no-progress --${{ matrix.stability }} + + - name: Execute tests + run: vendor/bin/pest + env: + DB_CONNECTION: mysql + DB_USERNAME: root + pgsql: runs-on: ubuntu-22.04 From 792e600f70a2c60f05079d628d49d36d076e69cb Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Sun, 7 Jan 2024 04:06:19 +1100 Subject: [PATCH 109/110] [1.x] Manually inline Livewire assets (#266) * Inline Livewire scripts * Remove unused scripts stack * Remove package lock to ensure assets are built with the latest versions * Update dependencies * Push scripts to the head * Allow local testsuite * Fix exception throwing --- package-lock.json | 722 ++++++++++++++++----- resources/views/components/pulse.blade.php | 5 +- src/Pulse.php | 10 +- 3 files changed, 566 insertions(+), 171 deletions(-) diff --git a/package-lock.json b/package-lock.json index 83fdd0c6..21cad914 100644 --- a/package-lock.json +++ b/package-lock.json @@ -378,6 +378,23 @@ "node": ">=12" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", @@ -417,9 +434,9 @@ "dev": true }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.19", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", - "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", + "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -467,6 +484,16 @@ "node": ">= 8" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@tailwindcss/container-queries": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@tailwindcss/container-queries/-/container-queries-0.1.1.tgz", @@ -477,9 +504,9 @@ } }, "node_modules/@tailwindcss/forms": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.6.tgz", - "integrity": "sha512-Fw+2BJ0tmAwK/w01tEFL5TiaJBX1NLT1/YbWgvm7ws3Qcn11kiXxzNTEQDMs5V3mQemhB56l3u0i9dwdzSQldA==", + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.7.tgz", + "integrity": "sha512-QE7X69iQI+ZXwldE+rzasvbJiyV/ju1FGHH0Qn2W3FKbuYtqp8LKcy6iSw79fVUT5/Vvf+0XgLCeYVG+UV6hOw==", "dev": true, "dependencies": { "mini-svg-data-uri": "^1.2.3" @@ -488,6 +515,30 @@ "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1" } }, + "node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", @@ -514,9 +565,9 @@ "dev": true }, "node_modules/autoprefixer": { - "version": "10.4.15", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.15.tgz", - "integrity": "sha512-KCuPB8ZCIqFdA4HwKXsvz7j6gvSDNhDP7WnUjBleRkKjPdvCmHFuQ77ocavI8FT6NdvlBnE2UFr2H4Mycn8Vew==", + "version": "10.4.16", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz", + "integrity": "sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==", "dev": true, "funding": [ { @@ -534,8 +585,8 @@ ], "dependencies": { "browserslist": "^4.21.10", - "caniuse-lite": "^1.0.30001520", - "fraction.js": "^4.2.0", + "caniuse-lite": "^1.0.30001538", + "fraction.js": "^4.3.6", "normalize-range": "^0.1.2", "picocolors": "^1.0.0", "postcss-value-parser": "^4.2.0" @@ -566,13 +617,12 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^1.0.0" } }, "node_modules/braces": { @@ -588,9 +638,9 @@ } }, "node_modules/browserslist": { - "version": "4.21.10", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz", - "integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==", + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz", + "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==", "dev": true, "funding": [ { @@ -607,10 +657,10 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001517", - "electron-to-chromium": "^1.4.477", - "node-releases": "^2.0.13", - "update-browserslist-db": "^1.0.11" + "caniuse-lite": "^1.0.30001565", + "electron-to-chromium": "^1.4.601", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" }, "bin": { "browserslist": "cli.js" @@ -629,9 +679,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001525", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001525.tgz", - "integrity": "sha512-/3z+wB4icFt3r0USMwxujAqRvaD/B7rvGTsKhbhSQErVrJvkZCLhgNLJxU8MevahQVH6hCU9FsHdNUFbiwmE7Q==", + "version": "1.0.30001574", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001574.tgz", + "integrity": "sha512-BtYEK4r/iHt/txm81KBudCUcTy7t+s9emrIaHqjYurQ10x71zJ5VQ9x1dYPcz/b+pKSp4y/v1xSI67A+LzpNyg==", "dev": true, "funding": [ { @@ -649,9 +699,9 @@ ] }, "node_modules/chart.js": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.0.tgz", - "integrity": "sha512-vQEj6d+z0dcsKLlQvbKIMYFHd3t8W/7L2vfJIbYcfyPcRx92CsHqECpueN8qVGNlKyDcr5wBrYAYKnfu/9Q1hQ==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.1.tgz", + "integrity": "sha512-C74QN1bxwV1v2PEujhmKjOZ7iUM4w6BWs23Md/6aOZZSlwMzeCIDGuZay++rBgChYru7/+QFeoQW0fQoP534Dg==", "dev": true, "dependencies": { "@kurkle/color": "^0.3.0" @@ -699,6 +749,24 @@ "node": ">= 6" } }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -708,11 +776,19 @@ "node": ">= 6" } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } }, "node_modules/cssesc": { "version": "3.0.0", @@ -738,10 +814,22 @@ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", "dev": true }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, "node_modules/electron-to-chromium": { - "version": "1.4.508", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.508.tgz", - "integrity": "sha512-FFa8QKjQK/A5QuFr2167myhMesGrhlOBD+3cYNxO9/S4XzHEXesyTD/1/xF644gC8buFPz3ca6G1LOQD0tZrrg==", + "version": "1.4.622", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.622.tgz", + "integrity": "sha512-GZ47DEy0Gm2Z8RVG092CkFvX7SdotG57c4YZOe8W8qD4rOmk3plgeNmiLVRHP/Liqj1wRiY3uUUod9vb9hnxZA==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true }, "node_modules/esbuild": { @@ -791,9 +879,9 @@ } }, "node_modules/fast-glob": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", - "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -819,9 +907,9 @@ } }, "node_modules/fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.16.0.tgz", + "integrity": "sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==", "dev": true, "dependencies": { "reusify": "^1.0.4" @@ -839,10 +927,26 @@ "node": ">=8" } }, + "node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/fraction.js": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.6.tgz", - "integrity": "sha512-n2aZ9tNfYDwaHhvFTkhFErqOMIb8uyzSQ+vGJBjZyanAKZVbGUQ1sngfk9FdkBw7G26O7AgNjLcecLffD1c7eg==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", "dev": true, "engines": { "node": "*" @@ -852,12 +956,6 @@ "url": "https://github.com/sponsors/rawify" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -873,26 +971,31 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", "dev": true, "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.17" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -910,34 +1013,18 @@ "node": ">=10.13.0" } }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", "dev": true, "dependencies": { - "function-bind": "^1.1.1" + "function-bind": "^1.1.2" }, "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" + "node": ">= 0.4" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -951,12 +1038,12 @@ } }, "node_modules/is-core-module": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", - "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", "dev": true, "dependencies": { - "has": "^1.0.3" + "hasown": "^2.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -971,6 +1058,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -992,10 +1088,34 @@ "node": ">=0.12.0" } }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jiti": { - "version": "1.19.3", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.19.3.tgz", - "integrity": "sha512-5eEbBDQT/jF1xg6l36P+mWGGoH9Spuy0PCdSr2dtWRDGC6ph/w9ZCL4lmESW8f8F7MwT3XKescfP0wnZWAKL9w==", + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", + "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", "dev": true, "bin": { "jiti": "bin/jiti.js" @@ -1016,6 +1136,15 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, + "node_modules/lru-cache": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz", + "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -1048,15 +1177,27 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", "dev": true, "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" } }, "node_modules/mz": { @@ -1071,9 +1212,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "dev": true, "funding": [ { @@ -1089,9 +1230,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", - "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", "dev": true }, "node_modules/normalize-path": { @@ -1130,22 +1271,13 @@ "node": ">= 6" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/path-parse": { @@ -1154,6 +1286,22 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/path-scurry": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", + "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "dev": true, + "dependencies": { + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -1191,9 +1339,9 @@ } }, "node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "version": "8.4.33", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz", + "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==", "dev": true, "funding": [ { @@ -1210,7 +1358,7 @@ } ], "dependencies": { - "nanoid": "^3.3.6", + "nanoid": "^3.3.7", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" }, @@ -1255,21 +1403,27 @@ } }, "node_modules/postcss-load-config": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.1.tgz", - "integrity": "sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "dependencies": { - "lilconfig": "^2.0.5", - "yaml": "^2.1.1" + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" }, "engines": { "node": ">= 14" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, "peerDependencies": { "postcss": ">=8.0.9", "ts-node": ">=9.0.0" @@ -1283,6 +1437,15 @@ } } }, + "node_modules/postcss-load-config/node_modules/lilconfig": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.0.0.tgz", + "integrity": "sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==", + "dev": true, + "engines": { + "node": ">=14" + } + }, "node_modules/postcss-nested": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", @@ -1303,9 +1466,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.0.13", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", - "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "version": "6.0.15", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz", + "integrity": "sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw==", "dev": true, "dependencies": { "cssesc": "^3.0.0", @@ -1363,9 +1526,9 @@ } }, "node_modules/resolve": { - "version": "1.22.4", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz", - "integrity": "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==", + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "dev": true, "dependencies": { "is-core-module": "^2.13.0", @@ -1390,9 +1553,9 @@ } }, "node_modules/rollup": { - "version": "3.28.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.28.1.tgz", - "integrity": "sha512-R9OMQmIHJm9znrU3m3cpE8uhN0fGdXiawME7aZIpQqvpS/85+Vt1Hq1/yVIcYfOmaQiHjvXkQAoJukvLpau6Yw==", + "version": "3.29.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", + "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", "dev": true, "bin": { "rollup": "dist/bin/rollup" @@ -1428,6 +1591,39 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/source-map-js": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", @@ -1437,15 +1633,111 @@ "node": ">=0.10.0" } }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/sucrase": { - "version": "3.34.0", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz", - "integrity": "sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==", + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", "dev": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", - "glob": "7.1.6", + "glob": "^10.3.10", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", @@ -1456,7 +1748,7 @@ "sucrase-node": "bin/sucrase-node" }, "engines": { - "node": ">=8" + "node": ">=16 || 14 >=14.17" } }, "node_modules/supports-preserve-symlinks-flag": { @@ -1472,9 +1764,9 @@ } }, "node_modules/tailwindcss": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.3.tgz", - "integrity": "sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.0.tgz", + "integrity": "sha512-VigzymniH77knD1dryXbyxR+ePHihHociZbXnLZHUyzf2MMs2ZVqlUrZ3FvpXP8pno9JzmILt1sZPD19M3IxtA==", "dev": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", @@ -1482,10 +1774,10 @@ "chokidar": "^3.5.3", "didyoumean": "^1.2.2", "dlv": "^1.1.3", - "fast-glob": "^3.2.12", + "fast-glob": "^3.3.0", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", - "jiti": "^1.18.2", + "jiti": "^1.19.1", "lilconfig": "^2.1.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", @@ -1548,9 +1840,9 @@ "dev": true }, "node_modules/update-browserslist-db": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", - "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", "dev": true, "funding": [ { @@ -1584,9 +1876,9 @@ "dev": true }, "node_modules/vite": { - "version": "4.4.9", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz", - "integrity": "sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==", + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.1.tgz", + "integrity": "sha512-AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQA==", "dev": true, "dependencies": { "esbuild": "^0.18.10", @@ -1638,16 +1930,116 @@ } } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yaml": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.2.tgz", - "integrity": "sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==", + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", + "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", "dev": true, "engines": { "node": ">= 14" diff --git a/resources/views/components/pulse.blade.php b/resources/views/components/pulse.blade.php index 64e37298..db90f748 100644 --- a/resources/views/components/pulse.blade.php +++ b/resources/views/components/pulse.blade.php @@ -13,10 +13,10 @@ {!! Laravel\Pulse\Facades\Pulse::css() !!} - @livewireStyles {!! Laravel\Pulse\Facades\Pulse::js() !!} + @livewireScriptConfig
@@ -49,8 +49,5 @@
- - @livewireScripts - @stack('scripts') diff --git a/src/Pulse.php b/src/Pulse.php index ade8fd76..f38897c6 100644 --- a/src/Pulse.php +++ b/src/Pulse.php @@ -520,11 +520,17 @@ public function css(string|Htmlable|array|null $css = null): string|self */ public function js(): string { - if (($content = file_get_contents(__DIR__.'/../dist/pulse.js')) === false) { + if ( + ($livewire = @file_get_contents(__DIR__.'/../../../livewire/livewire/dist/livewire.js')) === false && + ($livewire = @file_get_contents(__DIR__.'/../vendor/livewire/livewire/dist/livewire.js')) === false) { + throw new RuntimeException('Unable to load the Livewire JavaScript.'); + } + + if (($pulse = @file_get_contents(__DIR__.'/../dist/pulse.js')) === false) { throw new RuntimeException('Unable to load the Pulse dashboard JavaScript.'); } - return "".PHP_EOL; + return "".PHP_EOL."".PHP_EOL; } /** From b5e4b529c3a1a45d4713eb446ce7082c71a488e0 Mon Sep 17 00:00:00 2001 From: Jess Archer Date: Thu, 11 Jan 2024 10:54:41 +1000 Subject: [PATCH 110/110] Fix title attribute --- resources/views/livewire/slow-requests.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/livewire/slow-requests.blade.php b/resources/views/livewire/slow-requests.blade.php index 529a5d71..3a585bc6 100644 --- a/resources/views/livewire/slow-requests.blade.php +++ b/resources/views/livewire/slow-requests.blade.php @@ -51,7 +51,7 @@ {{ $slowRequest->uri }} @if ($slowRequest->action) -

+

{{ $slowRequest->action }}

@endif