Skip to content

feat: add type and outcome event dimensions - #30

Open
lohanidamodar wants to merge 3 commits into
mainfrom
feat/type-outcome-dimensions
Open

feat: add type and outcome event dimensions#30
lohanidamodar wants to merge 3 commits into
mainfrom
feat/type-outcome-dimensions

Conversation

@lohanidamodar

Copy link
Copy Markdown
Contributor

Adds two metric-scoped event dimensions: type and outcome.

Why

Today a category has to be encoded into the metric name and parsed back out by the consumer:

  • auth.method.phone.{countryCode}~190 metric names for one concept. Billing can't group by it, so it enumerates every calling code into an IN list.
  • messages.{type}.{provider}[.sent|.failed] — a cross-product of nine names built from three numbers (recipients, delivered, failed), at three granularities.
  • webhooks.events.{sent|failed} — the same outcome split again.

With these two columns, the first becomes GROUP BY metric, type and the second collapses to two rows tagged type+outcome.

Metric-scoped by design

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's the shape resourceId already has, where a bucket id and a function id share one column scoped by resourceType.

Deliberately not reusing status: it holds HTTP status, and sent/failed sort above 400 lexicographically, so an error filter (greaterThanEqual("status", "400")) would silently match every message row.

Details

  • set(0) indexed, like the other small closed value spaces (status, method, service, clientType). set supports ranges as well as equality, unlike bloom_filter.
  • LowCardinality(Nullable(String)) — each metric contributes a handful of values.
  • Sized type 64 / outcome 32.
  • Gauges unchanged. GAUGE_COLUMNS doesn't include them, so the gauge path still rejects both as unknown tags — verified.
  • setup() already emits ALTER TABLE … ADD COLUMN IF NOT EXISTS, so existing tables pick them up without a migration.

Verification

Confirmed end to end, since extractColumns() is strict (unknown tags throw rather than being dropped):

type     in EVENT_COLUMNS: yes
outcome  in EVENT_COLUMNS: yes
extractColumns: OK (type=sms outcome=sent)
gauge: correctly rejects type
index type       -> set(0)
index outcome    -> set(0)

And the query shapes the consumers need actually compile:

SELECT metric, SUM(value) as value, `type`
FROM `default`.`usage_events`
WHERE `metric` = {param_0:String} AND `time` >=AND `time` <GROUP BY metric, `type` ORDER BY value DESC

SELECT metric, SUM(value) as value, `type`, `outcome`
FROM `default`.`usage_events`
WHERE `metric` = {param_0:String}
GROUP BY metric, `type`, `outcome`

MetricTest::testEventColumnsConstant pins the column list and failed until updated — that guard did its job. Unit/schema/column-type tests green (56 tests, 288 assertions), Pint clean, PHPStan level max clean.

Not run: the ClickHouse e2e suite — no Docker daemon on this box. The DDL and query construction are verified above; actual execution against ClickHouse is CI's.

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.
@greptile-apps

greptile-apps Bot commented Aug 23, 2026

Copy link
Copy Markdown

Greptile Summary

The PR adds metric-scoped category and outcome dimensions for events while keeping gauges unchanged.

  • Adds the dimensions to the shared event contract and Database schema definition.
  • Adds low-cardinality ClickHouse columns and set indexes.
  • Updates schema and event-column assertions.

Confidence Score: 4/5

The PR is not yet safe to merge because upgraded Database-backed installations do not add the new attributes before tagged events are written.

Existing Database collections bypass schema creation after the duplicate-collection result, while the write path now emits category and outcome attributes, leaving upgraded deployments unable to persist the new event shape reliably.

Files Needing Attention: src/Usage/Adapter/Database.php and src/Usage/Metric.php

Important Files Changed

Filename Overview
src/Usage/Metric.php Adds category and outcome to the event dimension contract, schema, and indexes; existing Database collections still lack an upgrade path for the new attributes.
src/Usage/Adapter/ClickHouse.php Classifies category and outcome as low-cardinality event dimensions for ClickHouse storage.
tests/Usage/Adapter/ClickHouseSchemaTest.php Updates the expected ClickHouse event schema with category and outcome.
tests/Usage/MetricTest.php Updates the expected event-dimension constant with category and outcome.

Reviews (3): Last reviewed commit: "Merge branch 'main' into feat/type-outco..." | Re-trigger Greptile

Comment thread src/Usage/Metric.php
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.
@lohanidamodar

Copy link
Copy Markdown
Contributor Author

CI caught a real bug — thanks, tests. Fixed in cd214da.

The collision: the SQL adapter writes a structural type column holding 'event' or 'gauge', and builds its document as

array_merge(['type' => $type, /* ... */], $columns)   // Adapter/Database.php:175

$columns comes from extractColumns() and is the second argument, so a dimension named type silently overwrote the metric type — event rows were being written as type='sms' instead of type='event'. That's the 40 DatabaseTest errors, and it would have corrupted every row rather than failing loudly.

Renamed the dimension to category, which is free in both adapters. outcome was never in conflict.

Verified the merge now preserves the structural column:

structural type after merge: 'event'  (must be "event")
category=sms outcome=sent
category in EVENT_COLUMNS: yes | type in EVENT_COLUMNS: no

The separate schema-assertion failure was a second issue: ClickHouseSchemaTest keeps its own copy of the adapter's low-cardinality list, so adding columns there needs both lists updated. Done — though that duplication is a trap worth removing, since the test can't currently catch a divergence it mirrors by hand.

Worth noting for the record: I flagged in the PR description that the ClickHouse e2e suite hadn't run locally (no Docker here) and that execution was CI's job. It was, and it earned its keep — neither of these was reachable from the schema/query-construction checks I could run.

Comment thread src/Usage/Metric.php
Comment on lines +683 to +684
$stringColumn('category', 64),
$stringColumn('outcome', 32),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Existing Database schemas stay stale

If a Database-backed installation already has its event collection, Database::setup() swallows the duplicate-collection result without adding these new attributes, while addBatch() starts including them in documents. Consequently, upgraded deployments cannot reliably persist events containing category or outcome.

Knowledge Base Used:

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/Usage/Metric.php
Line: 683-684

Comment:
**Existing Database schemas stay stale**

If a Database-backed installation already has its event collection, `Database::setup()` swallows the duplicate-collection result without adding these new attributes, while `addBatch()` starts including them in documents. Consequently, upgraded deployments cannot reliably persist events containing `category` or `outcome`.

**Knowledge Base Used:**
- [Usage domain model](https://app.greptile.com/appwrite/-/custom-context/knowledge-base/utopia-php/usage/-/docs/usage-domain-model.md)
- [ClickHouse usage adapter](https://app.greptile.com/appwrite/-/custom-context/knowledge-base/utopia-php/usage/-/docs/clickhouse-usage-adapter.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code Fix in Codex

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant