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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions tests/Feature/CacheKeyGroupsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php

declare(strict_types=1);

namespace Cbox\StatamicTelemetry\Tests\Feature;

use Cbox\StatamicTelemetry\Tests\TestCase;
use Illuminate\Support\Facades\Cache;

/**
* The classifier is unit-tested key by key in CacheKeysTest; this covers
* the two things that only show up once it is wired to the core cache
* counter: that Hooks::register actually installs it, and that a
* Stache-scale keyspace really does collapse to a handful of series
* rather than one per key.
*/
class CacheKeyGroupsTest extends TestCase
{
protected function defineEnvironment($app)
{
parent::defineEnvironment($app);

// Cache counters are off by default in the core package, and the
// instrumentation reads the flag at boot.
$app['config']->set('telemetry.instrument.cache', true);
$app['config']->set('cache.default', 'array');
}

public function test_a_stache_scale_keyspace_collapses_to_bounded_key_groups(): void
{
$fake = $this->fakeTelemetry();

$keys = $this->keyspace();

// Three operations per key: a cold read, a write, a warm read.
foreach ($keys as $key) {
Cache::get($key);
Cache::put($key, 'value');
Cache::get($key);
}

$operations = $fake->recordedMetrics('cache.operations');

$operations
->assertLabelValues('key_group', [
'stache.index',
'stache.item',
'stache.meta',
'static_cache',
'static_cache.nocache',
'app',
])
// Six groups x three operations x one store. Leaking the raw
// key — or any per-collection/per-id fragment of it — into the
// label turns this into hundreds of series.
->assertCardinalityBelow(25)
->assertLabelCardinalityBelow('key_group', 10);

// Nothing was dropped: a classifier that returned null for a key
// would silently lose that key's operations from the counter, and
// the labelset would stop being comparable across stores.
$this->assertSame(3.0 * count($keys), $operations->total());
}

/**
* A warm Stache's cache traffic, at the shape (not the volume) a real
* site produces: many ids under a few bounded prefixes.
*
* @return list<string>
*/
private function keyspace(): array
{
$keys = ['stache::timing', 'stache::timestamps::entries', 'some-app-key'];

foreach (['blog', 'pages', 'docs', 'events'] as $collection) {
foreach (['title', 'slug', 'uri', 'published'] as $index) {
$keys[] = "stache::indexes::collections::{$collection}::{$index}";
}

foreach (range(1, 10) as $id) {
$keys[] = "stache::items::collections::{$collection}::entry-{$id}";
}

$keys[] = "static-cache:responses:{$collection}-index";
$keys[] = "nocache::session.{$collection}";
}

return $keys;
}
}
34 changes: 10 additions & 24 deletions tests/Feature/FrontendRequestTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,38 +48,24 @@

// http.route now carries the logical content route (via the core
// resolveRouteUsing hook), so route tables and histograms group by it
// instead of the /{segments?} catch-all.
$routes = [];
foreach ($fake->collect() as $family) {
if ($family->name() === 'http.server.request.duration') {
foreach ($family->samples as $sample) {
$routes[] = $sample->labels['http.route'] ?? null;
}
}
}

expect($routes)->toContain('entry:pages.page')
->and($routes)->not->toContain('/{segments?}');
// instead of the /{segments?} catch-all. Exactly one series — the
// catch-all must not survive *beside* the logical route either.
$fake->recordedMetrics('http.server.request.duration')
->assertLabelValues('http.route', ['entry:pages.page'])
->assertSeriesCount(1);
});

test('a Statamic frontend 404 is bucketed as not_found', function () {
$fake = $this->fakeTelemetry();

$this->get('/definitely-missing')->assertNotFound();

$routes = [];
foreach ($fake->collect() as $family) {
if ($family->name() === 'http.server.request.duration') {
foreach ($family->samples as $sample) {
$routes[] = $sample->labels['http.route'] ?? null;
}
}
}

// 404 traffic (broken links, bots) gets its own bounded bucket instead
// of polluting the /{segments?} catch-all.
expect($routes)->toContain('not_found')
->and($routes)->not->toContain('/{segments?}');
// of polluting the /{segments?} catch-all — and gets nothing else: a
// per-URI fallback would show up here as extra series.
$fake->recordedMetrics('http.server.request.duration')
->assertLabelValues('http.route', ['not_found'])
->assertSeriesCount(1);

// The raw catch-all template is still preserved on the span.
$span = array_values(array_filter(
Expand Down
25 changes: 19 additions & 6 deletions tests/Feature/ListenersTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,22 @@
use Statamic\Facades\Search;
use Statamic\Facades\User;

test('glide generations are counted per preset', function () {
test('glide generations are counted per preset, ad-hoc manipulations grouped', function () {
$fake = $this->fakeTelemetry();

event(new GlideImageGenerated('img/hero.jpg', ['p' => 'thumbnail']));

// Ad-hoc manipulations are unbounded in the wild — every width a
// template asks for is another param set. They must all collapse into
// the one "custom" bucket instead of becoming label values, so the
// three below have to stay two series in total.
event(new GlideImageGenerated('img/hero.jpg', ['w' => 100]));
event(new GlideImageGenerated('img/hero.jpg', ['w' => 640, 'h' => 480]));
event(new GlideImageGenerated('img/other.jpg', ['fit' => 'crop_focal']));

$fake->assertCounterIncremented('statamic.glide.generations', ['preset' => 'thumbnail']);
$fake->assertCounterIncremented('statamic.glide.generations', ['preset' => 'custom']);
$fake->recordedMetrics('statamic.glide.generations')
->assertLabelValues('preset', ['thumbnail', 'custom'])
->assertSeriesCount(2);
});

test('form submissions are counted per form', function () {
Expand Down Expand Up @@ -71,9 +79,14 @@
event(new TwoFactorAuthenticationFailed($user));
event(new ImpersonationStarted($user, $user));

$fake->assertCounterIncremented('statamic.auth.events', ['event' => 'user_registered']);
$fake->assertCounterIncremented('statamic.auth.events', ['event' => 'two_factor_failed']);
$fake->assertCounterIncremented('statamic.auth.events', ['event' => 'impersonation_started']);
// The vocabulary is the explicit map in RecordAuthEvent — three events
// in, exactly three label values out. A fallback that labelled an
// unmapped event by its class name would show up as a fourth.
$fake->assertMetricLabelValues('statamic.auth.events', 'event', [
'user_registered',
'two_factor_failed',
'impersonation_started',
]);
});

test('glide cache clears are counted by scope', function () {
Expand Down
15 changes: 8 additions & 7 deletions tests/Feature/MetricsProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@
Entry::make()->collection('pages')->slug('one')->save();
Entry::make()->collection('pages')->slug('two')->save();

$families = collect($fake->collect());
// One series per collection. Reading samples[0] could not see a
// duplicate `pages` series or a stray extra collection — either of
// which doubles the entry total on a dashboard.
$entries = $fake->recordedMetrics('statamic.entries.count')
->assertLabelValues('collection', ['pages'])
->assertSeriesCount(1);

$entries = $families->first(fn ($family) => $family->name() === 'statamic.entries.count');
expect($entries->total())->toBe(2.0);

expect($entries)->not->toBeNull()
->and($entries->samples[0]->labels)->toBe(['collection' => 'pages'])
->and($entries->samples[0]->value)->toBe(2.0);

expect($families->first(fn ($family) => $family->name() === 'statamic.users.count'))->not->toBeNull();
expect($fake->recordedMetrics('statamic.users.count'))->not->toBeEmpty();
});
29 changes: 29 additions & 0 deletions tests/Feature/PublishStateTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,32 @@

$fake->assertCounterIncremented('statamic.content.changes', ['type' => 'collection', 'action' => 'saved']);
});

test('the publish-state vocabulary is complete and mutually exclusive in one run', function () {
// The per-state tests above each save one entry, so they cannot see a
// status that is resolved once and reused, or a state that quietly
// collapses into a neighbour: every one of them still passes when all
// four saves land on the same action. Saving all four in one run and
// pinning the observed set does catch that — and covers `expired`,
// which the snapshot supports but no single-state test exercises.
Collection::make('news')->dated(true)
->futureDateBehavior('private')
->pastDateBehavior('private')
->save();

$fake = $this->fakeTelemetry();

Entry::make()->collection('pages')->slug('e')->published(true)->save();
Entry::make()->collection('pages')->slug('f')->published(false)->save();
Entry::make()->collection('news')->slug('g')->published(true)
->date(now()->addWeek()->format('Y-m-d-Hi'))->save();
Entry::make()->collection('news')->slug('h')->published(true)
->date(now()->subWeek()->format('Y-m-d-Hi'))->save();

$fake->recordedMetrics('statamic.content.changes')
->assertLabelValues('action', ['published', 'draft', 'scheduled', 'expired'])
// Entry saves never fall back to the generic `entry/saved` pair,
// and nothing else got attributed to this counter.
->assertLabelValues('type', ['entry'])
->assertSeriesCount(4);
});
16 changes: 12 additions & 4 deletions tests/Feature/StaticCacheTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,12 @@ function serving(Request $request): Request
$span->end();
$fake->flush();

$fake->assertCounterIncremented('statamic.static_cache.operations', ['operation' => 'miss']);
$fake->assertCounterIncremented('statamic.static_cache.operations', ['operation' => 'write']);
// A probe then a write: exactly those two outcomes. The per-request
// dedupe only remembers the *last* outcome, so a miss recorded again
// after the write would slip past a membership assertion.
$fake->recordedMetrics('statamic.static_cache.operations')
->assertLabelValues('operation', ['miss', 'write'])
->assertSeriesCount(2);

$fake->assertSpanRecorded('GET /about', fn ($span) => $span->attributes()['statamic.static_cache'] === 'write');
});
Expand Down Expand Up @@ -108,8 +112,12 @@ function serving(Request $request): Request
$cacher->invalidateUrl('/about');
$cacher->flush();

$fake->assertCounterIncremented('statamic.static_cache.operations', ['operation' => 'invalidate']);
$fake->assertCounterIncremented('statamic.static_cache.operations', ['operation' => 'flush']);
// Two purge calls, two outcomes. Statamic's invalidation paths call
// into each other, so the point is as much that invalidateUrl did not
// *also* get counted as a flush (or a miss) as that both fired.
$fake->recordedMetrics('statamic.static_cache.operations')
->assertLabelValues('operation', ['invalidate', 'flush'])
->assertSeriesCount(2);
});

test('the trace id header is stripped before the application cacher snapshots headers', function () {
Expand Down
Loading