Discussions aren't enabled on the repo, so opening this as a question issue rather than a bug report — I'd rather check the intent before sending a PR that might be arguing against a decision you already made deliberately.
What I was looking at
A report has a metric field (sum | average | min | max, default sum) and IChartMetric/zMetric additionally allow count. For the Metric chart type, that field appears to have no effect: apps/start/src/components/report-chart/metric/chart.tsx renders
<MetricCard key={serie.id} serie={serie} metric={'count'} unit={unit} />
MetricCard itself reads serie.metrics[metric] correctly, and serie.metrics already carries { count, sum, average, min, max } for every series (packages/db/src/engine/format.ts). There's also no UI anywhere to change a report's metric — no changeMetric action in reportSlice.ts, and no metric case in sidebar/ReportSettings.tsx.
My first read was "wiring bug". Then I found 84fd5ce ("fix: metric chart total count"), which changed it in exactly that direction on purpose:
- report: { metric, unit },
+ report: { unit },
- metric={metric}
+ metric={'count'}
…and in the same commit added count to the metrics constant, added uniq(profile_id) as total_count to the chart SQL, and added the count → sum coercion in report.ts. So the hardcoded count looks like the point of that commit, not an oversight.
The actual question
- Is
report.metric intentionally inert for chartType: 'metric' — i.e. a Metric card should always be the total unique count, and the field is only meaningful to other chart types?
- Or was hardcoding
count a stopgap for the total-count fix, with a per-report aggregation picker still wanted?
Happy to go either way; I just don't want to guess.
A separate thing that looks unambiguously wrong
Independent of the above: count is accepted by the public schema but cannot be stored.
zMetric (packages/validation/src/index.ts) includes count, and zReportInput.metric defaults to 'sum'.
- The Postgres enum has no
count: CREATE TYPE "Metric" AS ENUM ('sum', 'average', 'min', 'max') (migrations/20240131212106_add_metric/migration.sql).
- So
report.create / report.update silently rewrite it: metric: report.metric === 'count' ? 'sum' : report.metric (packages/trpc/src/routers/report.ts:68 and :115).
A caller that posts metric: 'count' gets a 200 and a report persisted as sum — no zod error, no warning. This is reachable from the agent/MCP generate_report tool, whose schema explicitly allows it: metric: z.enum(['sum', 'count', 'average']) (apps/api/src/agents/tools/base.ts:261). Also worth noting report.ts:188 (duplicate) doesn't have the coercion the other two paths do; it's safe today only because the value comes from the DB.
Whatever the answer to the design question, the schema advertising a value the storage can't hold seems worth closing — either by adding count to the enum, or by narrowing the persisted type so it's rejected instead of silently downgraded.
What I'd like to do (and the caveat)
If you're open to the picker, I have a change ready that:
- adds a
changeMetric action to reportSlice.ts (mirroring changeUnit);
- adds an aggregation picker to
ReportSettings.tsx for chartType === 'metric', reusing the labels already used by the report table (Unique / Sum / Average / Min / Max);
- passes
report.metric through in metric/chart.tsx;
- adds
count to the Metric enum (ALTER TYPE "Metric" ADD VALUE 'count') and drops the count → sum coercion, so a picked count actually persists.
Being upfront: step 3 reverses the metric={'count'} line from 84fd5ce. My argument for why that's safe given steps 1, 2 and 4 — and why it needs them — is that count stops being a hardcoded constant and becomes a stored, user-visible choice, so the total-unique-count behaviour that commit was fixing remains reachable and becomes explicit rather than implicit.
The part that needs your judgement is existing data. reports.metric is NOT NULL DEFAULT 'sum', so every existing Metric-type report stores 'sum', and there's no way to distinguish "user chose sum" from "never touched". Passing report.metric straight through would therefore flip every existing Metric card from count to sum — and those aren't near-neighbours: count is uniq(profile_id) for the whole range, while sum is the sum of per-bucket values, where the bucket is count(*) for a default event series. A tile currently reading ~1.2k unique users could read ~45k events instead (and for property_sum series they aren't even the same unit).
So I'd pair it with a backfill — UPDATE reports SET metric = 'count' WHERE "chartType" = 'metric' — so no existing dashboard number changes. That looks lossless because for Metric reports the stored value is currently read by nothing: the SQL builders ignore metric (your own comment in chart-sql.test.ts notes it's a display-only field), and the render hardcodes count. But it is still a data migration on user data, so I'd rather you sign off than surprise you with it in a PR.
(One implementation note if you do want it: the ALTER TYPE ... ADD VALUE and the UPDATE have to be in two separate migration files — Postgres won't let a new enum value be used in the transaction that added it.)
Some smaller things I noticed while tracing this, happy to split into their own issues if useful:
getAggregateChartSql never selects total_count, so bar/pie series always have count: undefined and the report table's "Unique" column is permanently blank for them.
format.ts uses .find(item => !!item.total_count), so a genuine 0 is indistinguishable from missing and renders as N/A.
- Series are ranked by
metrics.sum in format.ts while the Metric card displays metrics.count, so which cards survive the 4-card cap is decided by a number that isn't the one shown.
Thanks — happy to close this if the answer is "working as intended".
Discussions aren't enabled on the repo, so opening this as a
questionissue rather than a bug report — I'd rather check the intent before sending a PR that might be arguing against a decision you already made deliberately.What I was looking at
A report has a
metricfield (sum | average | min | max, defaultsum) andIChartMetric/zMetricadditionally allowcount. For the Metric chart type, that field appears to have no effect:apps/start/src/components/report-chart/metric/chart.tsxrendersMetricCarditself readsserie.metrics[metric]correctly, andserie.metricsalready carries{ count, sum, average, min, max }for every series (packages/db/src/engine/format.ts). There's also no UI anywhere to change a report'smetric— nochangeMetricaction inreportSlice.ts, and nometriccase insidebar/ReportSettings.tsx.My first read was "wiring bug". Then I found 84fd5ce ("fix: metric chart total count"), which changed it in exactly that direction on purpose:
…and in the same commit added
countto themetricsconstant, addeduniq(profile_id) as total_countto the chart SQL, and added thecount → sumcoercion inreport.ts. So the hardcodedcountlooks like the point of that commit, not an oversight.The actual question
report.metricintentionally inert forchartType: 'metric'— i.e. a Metric card should always be the total unique count, and the field is only meaningful to other chart types?counta stopgap for the total-count fix, with a per-report aggregation picker still wanted?Happy to go either way; I just don't want to guess.
A separate thing that looks unambiguously wrong
Independent of the above:
countis accepted by the public schema but cannot be stored.zMetric(packages/validation/src/index.ts) includescount, andzReportInput.metricdefaults to'sum'.count:CREATE TYPE "Metric" AS ENUM ('sum', 'average', 'min', 'max')(migrations/20240131212106_add_metric/migration.sql).report.create/report.updatesilently rewrite it:metric: report.metric === 'count' ? 'sum' : report.metric(packages/trpc/src/routers/report.ts:68and:115).A caller that posts
metric: 'count'gets a200and a report persisted assum— no zod error, no warning. This is reachable from the agent/MCPgenerate_reporttool, whose schema explicitly allows it:metric: z.enum(['sum', 'count', 'average'])(apps/api/src/agents/tools/base.ts:261). Also worth notingreport.ts:188(duplicate) doesn't have the coercion the other two paths do; it's safe today only because the value comes from the DB.Whatever the answer to the design question, the schema advertising a value the storage can't hold seems worth closing — either by adding
countto the enum, or by narrowing the persisted type so it's rejected instead of silently downgraded.What I'd like to do (and the caveat)
If you're open to the picker, I have a change ready that:
changeMetricaction toreportSlice.ts(mirroringchangeUnit);ReportSettings.tsxforchartType === 'metric', reusing the labels already used by the report table (Unique / Sum / Average / Min / Max);report.metricthrough inmetric/chart.tsx;countto theMetricenum (ALTER TYPE "Metric" ADD VALUE 'count') and drops thecount → sumcoercion, so a pickedcountactually persists.Being upfront: step 3 reverses the
metric={'count'}line from 84fd5ce. My argument for why that's safe given steps 1, 2 and 4 — and why it needs them — is thatcountstops being a hardcoded constant and becomes a stored, user-visible choice, so the total-unique-count behaviour that commit was fixing remains reachable and becomes explicit rather than implicit.The part that needs your judgement is existing data.
reports.metricisNOT NULL DEFAULT 'sum', so every existing Metric-type report stores'sum', and there's no way to distinguish "user chose sum" from "never touched". Passingreport.metricstraight through would therefore flip every existing Metric card fromcounttosum— and those aren't near-neighbours:countisuniq(profile_id)for the whole range, whilesumis the sum of per-bucket values, where the bucket iscount(*)for a default event series. A tile currently reading ~1.2k unique users could read ~45k events instead (and forproperty_sumseries they aren't even the same unit).So I'd pair it with a backfill —
UPDATE reports SET metric = 'count' WHERE "chartType" = 'metric'— so no existing dashboard number changes. That looks lossless because for Metric reports the stored value is currently read by nothing: the SQL builders ignoremetric(your own comment inchart-sql.test.tsnotes it's a display-only field), and the render hardcodescount. But it is still a data migration on user data, so I'd rather you sign off than surprise you with it in a PR.(One implementation note if you do want it: the
ALTER TYPE ... ADD VALUEand theUPDATEhave to be in two separate migration files — Postgres won't let a new enum value be used in the transaction that added it.)Some smaller things I noticed while tracing this, happy to split into their own issues if useful:
getAggregateChartSqlnever selectstotal_count, so bar/pie series always havecount: undefinedand the report table's "Unique" column is permanently blank for them.format.tsuses.find(item => !!item.total_count), so a genuine0is indistinguishable from missing and renders asN/A.metrics.suminformat.tswhile the Metric card displaysmetrics.count, so which cards survive the 4-card cap is decided by a number that isn't the one shown.Thanks — happy to close this if the answer is "working as intended".