diff --git a/CHANGELOG.md b/CHANGELOG.md index 8978a2f..f01de9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 0.11.0 — sdk dimensions + +### Added + +- Two event-only SDK dimensions in `Metric::EVENT_COLUMNS` and + `Metric::getEventSchema()`: `sdk` (originating SDK name, e.g. + `web`, `flutter`, `console`, `cli`) and `sdkVersion` (e.g. + `14.0.0`). Both are optional strings. In ClickHouse both map to + `LowCardinality(Nullable(String))` with `CODEC(ZSTD(3))`. Existing + tables auto-materialize the columns on `setup()` via the + `ADD COLUMN IF NOT EXISTS` path. Gauges are unchanged; these columns + are not added to the primary key or indexes. + ## 0.10.0 — premium geo dimensions ### Added diff --git a/src/Usage/Adapter/ClickHouse.php b/src/Usage/Adapter/ClickHouse.php index a1f383f..8460166 100644 --- a/src/Usage/Adapter/ClickHouse.php +++ b/src/Usage/Adapter/ClickHouse.php @@ -1213,6 +1213,8 @@ private function getColumnType(string $id, string $type = 'event'): string // are high-cardinality and intentionally fall through to Nullable(String)) 'continentCode', 'subdivisions', 'connectionType', 'connectionUsageType', 'autonomousSystemNumber', + // sdk identity + 'sdk', 'sdkVersion', ]; if (in_array($id, $lowCardinality, true)) { @@ -1256,6 +1258,7 @@ private function getColumnCodec(string $id): string 'city', 'continentCode', 'subdivisions', 'isp', 'autonomousSystemNumber', 'autonomousSystemOrganization', 'connectionType', 'connectionUsageType', 'connectionOrganization', + 'sdk', 'sdkVersion', ]; if (in_array($id, $zstdColumns, true)) { diff --git a/src/Usage/Metric.php b/src/Usage/Metric.php index 734a674..416efcc 100644 --- a/src/Usage/Metric.php +++ b/src/Usage/Metric.php @@ -61,6 +61,8 @@ class Metric extends ArrayObject 'osCode', 'osName', 'osVersion', 'clientType', 'clientCode', 'clientName', 'clientVersion', 'clientEngine', 'clientEngineVersion', + // sdk identity + 'sdk', 'sdkVersion', 'deviceName', 'deviceBrand', 'deviceModel', ]; @@ -92,6 +94,7 @@ class Metric extends ArrayObject * - osCode / osName / osVersion: parsed user-agent OS fields * - clientType / clientCode / clientName / clientVersion: parsed client * - clientEngine / clientEngineVersion: parsed client engine + * - sdk / sdkVersion: originating SDK name and version * - deviceName / deviceBrand / deviceModel: parsed device fields * * Gauge-only dimension columns (see GAUGE_COLUMNS): @@ -654,6 +657,9 @@ public static function getEventSchema(): array $stringColumn('clientVersion', 255), $stringColumn('clientEngine', 256), $stringColumn('clientEngineVersion', 255), + // sdk identity + $stringColumn('sdk', 256), + $stringColumn('sdkVersion', 255), $stringColumn('deviceName', 256), $stringColumn('deviceBrand', 256), $stringColumn('deviceModel', 255), diff --git a/tests/Usage/Adapter/ClickHouseColumnTypeTest.php b/tests/Usage/Adapter/ClickHouseColumnTypeTest.php index a56e7a6..834015e 100644 --- a/tests/Usage/Adapter/ClickHouseColumnTypeTest.php +++ b/tests/Usage/Adapter/ClickHouseColumnTypeTest.php @@ -72,4 +72,18 @@ public function testHighCardinalityPremiumGeoColumns(): void ); } } + + /** + * SDK dims must map to LowCardinality(Nullable(String)). + */ + public function testLowCardinalitySdkColumns(): void + { + foreach (['sdk', 'sdkVersion'] as $col) { + $this->assertSame( + 'LowCardinality(Nullable(String))', + $this->columnType($col), + "{$col} should be LowCardinality(Nullable(String))" + ); + } + } } diff --git a/tests/Usage/Adapter/ClickHouseSchemaTest.php b/tests/Usage/Adapter/ClickHouseSchemaTest.php index 01c2339..94e8618 100644 --- a/tests/Usage/Adapter/ClickHouseSchemaTest.php +++ b/tests/Usage/Adapter/ClickHouseSchemaTest.php @@ -51,6 +51,10 @@ public function testEventsTableCarriesCodecsAndLowCardinality(): void $this->assertStringContainsString('`connectionType` LowCardinality(Nullable(String)) CODEC(ZSTD(3))', $ddl); $this->assertStringContainsString('`city` Nullable(String) CODEC(ZSTD(3))', $ddl); $this->assertStringContainsString('`isp` Nullable(String) CODEC(ZSTD(3))', $ddl); + + // sdk: both dims are low-cardinality. + $this->assertStringContainsString('`sdk` LowCardinality(Nullable(String)) CODEC(ZSTD(3))', $ddl); + $this->assertStringContainsString('`sdkVersion` LowCardinality(Nullable(String)) CODEC(ZSTD(3))', $ddl); } public function testEventsTableSwapsBloomForSetOnLowCardinality(): void @@ -264,6 +268,7 @@ private function expectedDimAssertions(array $columns, string $type): array 'hostname', 'ip', 'continentCode', 'subdivisions', 'connectionType', 'connectionUsageType', 'autonomousSystemNumber', + 'sdk', 'sdkVersion', ]; $baseKey = ['id', 'metric', 'value', 'time', 'tenant']; diff --git a/tests/Usage/MetricTest.php b/tests/Usage/MetricTest.php index b27a2d2..718b148 100644 --- a/tests/Usage/MetricTest.php +++ b/tests/Usage/MetricTest.php @@ -615,6 +615,7 @@ public function testEventColumnsConstant(): void 'osCode', 'osName', 'osVersion', 'clientType', 'clientCode', 'clientName', 'clientVersion', 'clientEngine', 'clientEngineVersion', + 'sdk', 'sdkVersion', 'deviceName', 'deviceBrand', 'deviceModel', ]; $this->assertSame($expected, Metric::EVENT_COLUMNS); @@ -681,6 +682,35 @@ public function testPremiumGeoColumnsArePresentAsEventStrings(): void } } + /** + * The sdk dimensions are event-only string columns that are present in + * EVENT_COLUMNS and the event schema (as non-required strings) but never + * leak into the gauge column set. + */ + public function testSdkColumnsArePresentAsEventStrings(): void + { + $sdk = ['sdk', 'sdkVersion']; + + $schema = Metric::getEventSchema(); + $eventIds = array_column($schema, '$id'); + $gaugeIds = array_column(Metric::getGaugeSchema(), '$id'); + + foreach ($sdk as $col) { + $this->assertContains($col, Metric::EVENT_COLUMNS, "EVENT_COLUMNS missing {$col}"); + $this->assertContains($col, $eventIds, "Event schema missing {$col}"); + $this->assertNotContains($col, Metric::GAUGE_COLUMNS, "{$col} must be event-only"); + $this->assertNotContains($col, $gaugeIds, "{$col} must not be in gauge schema"); + + $matches = array_values(array_filter( + $schema, + static fn (array $attr): bool => $attr['$id'] === $col, + )); + $this->assertCount(1, $matches, "{$col} must appear exactly once"); + $this->assertSame('string', $matches[0]['type'], "{$col} must be a string column"); + $this->assertFalse($matches[0]['required'], "{$col} must be optional"); + } + } + /** * Test that the gauge schema contains the new team and resource id columns. */