Skip to content

mergeByDimensions joins its dimension key with no delimiter, so two distinct groups can merge into one row #4821

Description

@os-zhuang

Found while implementing #4708 (packages/services/service-analytics/src/dataset-executor.ts); unrelated to that fix, so filed rather than fixed in it.

Mechanism

mergeByDimensions is the seam every multi-query dataset result is assembled through — supplementary measure-scoped queries and the compareTo pass both merge back onto the grid with it. Its key function:

const keyOf = (row: Record<string, unknown>) =>
  dimensions.map((d) => String(row[d] ?? '')).join('');

Two problems in one line:

  1. No delimiter. With two or more dimensions, the concatenation is ambiguous. Rows { region: 'ab', segment: 'c' } and { region: 'a', segment: 'bc' } both key as "abc", so the second row's measures are merged onto the first — one row silently absorbs another group's numbers, and the other group's measure column is left absent (which then renders blank, or gets an empty-group 0 filled into it after A filtered dataset measure returns ABSENT (not 0) for a group its filter excludes, so every derived ratio over it blanks — on exactly the worst-performing row #4708).
  2. null and '' collapse. String(row[d] ?? '') maps a genuinely null dimension value and an empty-string one onto the same key, merging "unassigned" into "blank".

The neighbouring cross-object-rebucket.ts already solved exactly this and documents why:

// JSON-encoded, so the empty bucket (`null` on both aggregation paths since
// …) … `"null"` — plain interpolation renders both as `null` and would merge two
for (const f of baseDimFields) keyParts.push(`${f}=${JSON.stringify(row[f] ?? null)}`);

So the repo has the right pattern one file over; mergeByDimensions predates it.

Impact

Only bites datasets grouped by two or more dimensions where the concatenation of adjacent values is ambiguous — a matrix report by region × segment, owner × stage, or any pair of free-text/select values. Single-dimension grids (the common case) are unaffected, which is presumably why it has survived. When it does bite, it is silent: the grid keeps the right number of columns and a plausible number of rows.

Suggested direction

Key the same way cross-object-rebucket does — dimensions.map((d) => ${d}=${JSON.stringify(row[d] ?? null)}).join('&') — which fixes both the delimiter and the null/empty-string conflation in one change. Needs a test with a colliding pair ('ab'|'c' vs 'a'|'bc') and one with a null-vs-empty dimension value.

Note the fix changes only the internal join key, never a value in the response, so it is contained.

Related: #4708.

Metadata

Metadata

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions