From d2c10089134135bb616a5972a11d54bde8758691 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Sun, 23 Aug 2026 14:14:47 +0545 Subject: [PATCH 1/2] feat: add type and outcome event dimensions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two metric-scoped dimensions, so a metric can be broken down by what the row is about and how the attempt ended without encoding either into the metric name. They replace the pattern where a category becomes part of the name and the consumer parses it back out — auth.method.phone.{countryCode} is ~190 metric names for one concept, and messages.{type}.{provider}.{sent|failed} is a cross-product of nine names built from three numbers. Both are read with `metric` pinned, so the same column carries different value spaces across metrics — a messaging channel for one, a calling code for another. That is the shape resourceId already has, where a bucket id and a function id share a column scoped by resourceType. set(0) indexed like the other small closed value spaces, and LowCardinality since each metric contributes only a handful of values. Gauges are unchanged: GAUGE_COLUMNS does not include them, so the gauge path still rejects both as unknown tags. --- src/Usage/Adapter/ClickHouse.php | 2 ++ src/Usage/Metric.php | 12 +++++++++++- tests/Usage/MetricTest.php | 1 + 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Usage/Adapter/ClickHouse.php b/src/Usage/Adapter/ClickHouse.php index 37e29be..d0f10c7 100644 --- a/src/Usage/Adapter/ClickHouse.php +++ b/src/Usage/Adapter/ClickHouse.php @@ -1216,6 +1216,8 @@ private function getColumnType(string $id, string $type = 'event'): string 'clientType', 'clientCode', 'clientName', 'clientVersion', 'clientEngine', 'clientEngineVersion', 'deviceName', 'deviceBrand', 'deviceModel', + // metric-scoped dimensions; small closed value spaces per metric + 'type', 'outcome', 'hostname', 'ip', // premium geo (lower-cardinality only; city/isp/AS org/connection org // are high-cardinality and intentionally fall through to Nullable(String)) diff --git a/src/Usage/Metric.php b/src/Usage/Metric.php index 6c1eff9..ca04349 100644 --- a/src/Usage/Metric.php +++ b/src/Usage/Metric.php @@ -64,6 +64,8 @@ class Metric extends ArrayObject // sdk identity 'sdk', 'sdkVersion', 'deviceName', 'deviceBrand', 'deviceModel', + // metric-scoped: what the row is about, and how it ended + 'type', 'outcome', ]; /** @@ -670,6 +672,13 @@ public static function getEventSchema(): array // sdk identity $stringColumn('sdk', 256), $stringColumn('sdkVersion', 255), + // metric-scoped dimensions. `type` is whatever category the metric + // is broken down by — a messaging channel, a calling code — and + // `outcome` is how the attempt ended. Both are read with `metric` + // pinned, so the same column carries different value spaces across + // metrics, the way resourceId already carries ids of every kind. + $stringColumn('type', 64), + $stringColumn('outcome', 32), $stringColumn('deviceName', 256), $stringColumn('deviceBrand', 256), $stringColumn('deviceModel', 255), @@ -756,6 +765,7 @@ public static function getSchema(): array public static function getEventIndexes(): array { $indexed = [ + 'type', 'outcome', 'path', 'method', 'status', 'service', 'resourceType', 'resourceId', 'resourceInternalId', 'teamId', 'teamInternalId', @@ -763,7 +773,7 @@ public static function getEventIndexes(): array 'osName', 'clientType', 'clientName', 'deviceName', ]; - $setIndexed = ['status', 'method', 'country', 'service', 'clientType', 'osName']; + $setIndexed = ['status', 'method', 'country', 'service', 'clientType', 'osName', 'type', 'outcome']; return array_map( static function (string $col) use ($setIndexed): array { diff --git a/tests/Usage/MetricTest.php b/tests/Usage/MetricTest.php index fa7e3e6..f5e31d0 100644 --- a/tests/Usage/MetricTest.php +++ b/tests/Usage/MetricTest.php @@ -617,6 +617,7 @@ public function testEventColumnsConstant(): void 'clientEngine', 'clientEngineVersion', 'sdk', 'sdkVersion', 'deviceName', 'deviceBrand', 'deviceModel', + 'type', 'outcome', ]; $this->assertSame($expected, Metric::EVENT_COLUMNS); } From cd214dad2bad2351ec7140a782a22a2665264f13 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Tue, 25 Aug 2026 07:00:25 +0545 Subject: [PATCH 2/2] fix: name the dimension category, not type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI caught a real collision. The SQL adapter writes a structural `type` column holding 'event' or 'gauge', and builds its document as array_merge(['type' => $type, ...], $columns) with $columns coming from extractColumns(). A dimension named `type` lands in $columns, which is the second argument, so it silently overwrote the metric type — event rows were written as type='sms' instead of type='event'. That is what the 40 DatabaseTest errors were. Renamed to `category`, which is free in both adapters. `outcome` was never in conflict. Also updated the low-cardinality list in ClickHouseSchemaTest, which keeps its own copy of the adapter's list — that duplication is why the schema assertion failed separately from the collision. --- src/Usage/Adapter/ClickHouse.php | 2 +- src/Usage/Metric.php | 17 ++++++++++------- tests/Usage/Adapter/ClickHouseSchemaTest.php | 1 + tests/Usage/MetricTest.php | 2 +- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/Usage/Adapter/ClickHouse.php b/src/Usage/Adapter/ClickHouse.php index d0f10c7..8fdf5a0 100644 --- a/src/Usage/Adapter/ClickHouse.php +++ b/src/Usage/Adapter/ClickHouse.php @@ -1217,7 +1217,7 @@ private function getColumnType(string $id, string $type = 'event'): string 'clientEngine', 'clientEngineVersion', 'deviceName', 'deviceBrand', 'deviceModel', // metric-scoped dimensions; small closed value spaces per metric - 'type', 'outcome', + 'category', 'outcome', 'hostname', 'ip', // premium geo (lower-cardinality only; city/isp/AS org/connection org // are high-cardinality and intentionally fall through to Nullable(String)) diff --git a/src/Usage/Metric.php b/src/Usage/Metric.php index ca04349..6f93382 100644 --- a/src/Usage/Metric.php +++ b/src/Usage/Metric.php @@ -64,8 +64,11 @@ class Metric extends ArrayObject // sdk identity 'sdk', 'sdkVersion', 'deviceName', 'deviceBrand', 'deviceModel', - // metric-scoped: what the row is about, and how it ended - 'type', 'outcome', + // metric-scoped: what the row is about, and how it ended. + // Named `category`, not `type`: the SQL adapter already writes a `type` + // column holding 'event'/'gauge', and a dimension of that name silently + // overwrote it through array_merge. + 'category', 'outcome', ]; /** @@ -672,12 +675,12 @@ public static function getEventSchema(): array // sdk identity $stringColumn('sdk', 256), $stringColumn('sdkVersion', 255), - // metric-scoped dimensions. `type` is whatever category the metric - // is broken down by — a messaging channel, a calling code — and + // metric-scoped dimensions. `category` is whatever the metric is + // broken down by — a messaging channel, a calling code — and // `outcome` is how the attempt ended. Both are read with `metric` // pinned, so the same column carries different value spaces across // metrics, the way resourceId already carries ids of every kind. - $stringColumn('type', 64), + $stringColumn('category', 64), $stringColumn('outcome', 32), $stringColumn('deviceName', 256), $stringColumn('deviceBrand', 256), @@ -765,7 +768,7 @@ public static function getSchema(): array public static function getEventIndexes(): array { $indexed = [ - 'type', 'outcome', + 'category', 'outcome', 'path', 'method', 'status', 'service', 'resourceType', 'resourceId', 'resourceInternalId', 'teamId', 'teamInternalId', @@ -773,7 +776,7 @@ public static function getEventIndexes(): array 'osName', 'clientType', 'clientName', 'deviceName', ]; - $setIndexed = ['status', 'method', 'country', 'service', 'clientType', 'osName', 'type', 'outcome']; + $setIndexed = ['status', 'method', 'country', 'service', 'clientType', 'osName', 'category', 'outcome']; return array_map( static function (string $col) use ($setIndexed): array { diff --git a/tests/Usage/Adapter/ClickHouseSchemaTest.php b/tests/Usage/Adapter/ClickHouseSchemaTest.php index 4829c27..7943dcd 100644 --- a/tests/Usage/Adapter/ClickHouseSchemaTest.php +++ b/tests/Usage/Adapter/ClickHouseSchemaTest.php @@ -275,6 +275,7 @@ private function expectedDimAssertions(array $columns, string $type): array 'connectionUsageType', 'autonomousSystemNumber', 'sdk', 'sdkVersion', 'ordinal', + 'category', 'outcome', ]; $baseKey = ['id', 'metric', 'value', 'time', 'tenant']; diff --git a/tests/Usage/MetricTest.php b/tests/Usage/MetricTest.php index f5e31d0..9a0d69d 100644 --- a/tests/Usage/MetricTest.php +++ b/tests/Usage/MetricTest.php @@ -617,7 +617,7 @@ public function testEventColumnsConstant(): void 'clientEngine', 'clientEngineVersion', 'sdk', 'sdkVersion', 'deviceName', 'deviceBrand', 'deviceModel', - 'type', 'outcome', + 'category', 'outcome', ]; $this->assertSame($expected, Metric::EVENT_COLUMNS); }