fix(data-insights): declare the data asset types once and make the field catalog deterministic - #31937
fix(data-insights): declare the data asset types once and make the field catalog deterministic#31937manerow wants to merge 4 commits into
Conversation
✅ PR checks passedThe linked issue has a description and all required Shipping project fields set. Thanks! |
✅ TypeScript Types Auto-UpdatedThe generated TypeScript types have been automatically updated based on JSON schema changes in this PR. |
✅ Playwright Results — workflow succeededValidated commit ✅ 1280 passed · ❌ 0 failed · 🟡 3 flaky · ⏭️ 0 skipped · 🧰 0 lifecycle flaky PerformanceBlocking targets: ✅ met · Optimization targets: 🟡 in progress Shard-job maxima below are not the full workflow wall time; the linked run includes build, fixture, planning, and reporting. 🕒 Full workflow signal wall (to summary) 51m 15s ⏱️ Max setup 4m 24s · max shard execution 17m 50s · max shard-job elapsed before upload 21m 17s · reporting 8s 🌐 197.86 requests/attempt · 2.13 app boots/UI scenario · 29.44% common-shard skew Optimization targets still in progress:
🟡 3 flaky test(s) (passed on retry)
How to debug locally# Download playwright-test-results-<shard> artifact and unzip
npx playwright show-trace path/to/trace.zip # view trace |
…eld catalog deterministic
Code Review 👍 Approved with suggestions 4 resolved / 5 findingsCentralizes Data Insights asset types, makes field-catalog generation deterministic, and aligns filtered terms-axis category selection with the metric population. Consider addressing the minor formula population ranking observation. 💡 Edge Case: All-narrowed formula ranks axis by one operand's population📄 openmetadata-service/src/main/java/org/openmetadata/service/search/elasticsearch/dataInsightAggregators/ElasticSearchDynamicChartAggregatorInterface.java:118-132 📄 openmetadata-service/src/main/java/org/openmetadata/service/search/opensearch/dataInsightAggregator/OpenSearchDynamicChartAggregatorInterface.java:113-127 In getDateHistogramByFormula, when a metric has a filter and every formula term also carries its own q= (all wrappers land in ✅ 4 resolved✅ Performance: Per-type dedup scans the whole growing catalog (O(n^2))
✅ Quality: Test mutates global static SearchRepository, leaking across JVM fork
✅ Bug: Terms order path with dotted field name may fail server-side
✅ Quality: Inlined terms-axis builder duplicates include/exclude logic 4x
🤖 Prompt for agentsOptionsDisplay: compact → Showing less information. Comment with these commands to change the behavior for this request:
Was this helpful? React with 👍 / 👎 | Powered by Gitar — free for open source |
|
Superseded by a fresh PR from the same branch: the history here contained a request-narrowing approach that was later replaced by axis ranking, and the timeline no longer matches the code. Reopening clean. |
|
Continued in #32115 (same branch, rebuilt history). |
|
|



Fixes #31963
Three Data Insights bugs that share a shape: the chart returns a number that looks fine and is wrong. Nothing throws, nothing logs.
1.
metricnever reaches the chart builderThe set of entity types Data Insights covers was hand-written in three places:
DataInsightSystemChartRepository.dataAssetTypesDataInsightsApp.dataAssetTypesdataInsights/config.jsonkeysB and C stay in sync only because a mismatch throws. Nothing keeps A honest.
#26260 added
metricto B and C in March, to make metrics chartable. It was never added to A. Sodi-data-assets-metrichas been created and filled daily ever since, whilemetricType,unitOfMeasurementandgranularityhave never appeared in the chart builder.Fix: one enum,
dataAssetType.json. A derives from it. B derives as "every type that no live index aliases in", which is the same 16 as before, so ingestion is unchanged.B is deliberately not equal to A. The two data-quality types reach the wildcard through a
dataInsightAliasesentry pointing at the live index, so ingesting them would aim create/delete at live data.2. The catalog changes on every server restart
getFieldNamesdeduplicated field names globally, so the first entity type iterated claimed a shared name permanently. The iteration source was aSet.of, whose order Java salts per JVM start.Measured on a seeded instance: 277 records covering 14 of 17 types.
topicshares all 28 of its field names withtable, so it advertised zero fields despite holding 47,766 documents. Across the 68 possible orders, 9 outcomes are reachable and 17 types never is.Someone picking "Topic" in the field picker got an empty list on roughly a coin flip.
Fix: dedupe by
(entityType, name)instead ofname. The type set also becomes aLinkedHashSetin enum order, so record order is stable too, which matters for any consumer that resolves a duplicated name by taking the first record.3. Charts pick categories from data they don't count
A line chart with a terms x-axis picks its 100 buckets, then counts inside them with the metric filter applied. Selection ignored the filter.
So a "tables per service" chart ranked services by their total document count. A BI service with 5,000 dashboards and 3 tables outranked a warehouse service with 800 tables and nothing else. The first got a slot and rendered as
3. The second was missing from the chart entirely.The axis selected its categories by a quantity the chart neither filters on nor displays. Measured on a seeded instance: a "Tier 1 assets per service" chart put
test-dashboard-service(215,515 documents, zero Tier-1 assets) in the first slot, ahead of the one service that had any.Fix: when every metric of a chart shares one filter, apply it to the request as well as inside the buckets. Within a bucket the filter is a conjunct applied twice, so the predicate is unchanged; what moves is which categories get a bucket at all.
Gated on the chart having a terms x-axis. A
@timestampchart has no top-N to align, and narrowing its query would shorten the window a date histogram plots, moving the first/last delta the dashboard renders. Six shipped charts have that shape.4. An empty category takes the whole chart down with a 500
avg,minandmaxover a bucket the metric filter emptied returnnull, not0— there is no average of nothing. Both search clients hand that back as a boxedDouble, and both aggregators unbox it straight into a primitive:Nothing catches it, so the request dies and the user gets an HTTP 500 rather than a chart.
sumandcountreturn0.0, which is why only those three functions are affected.This is not new and it is not caused by this PR — reproduced on a
maindeployment with a three-line chart definition:It is fixed here because §3 makes it reachable again. Ranking by the measure deliberately keeps the categories the filter matched nothing in — exactly the buckets that return
null. Hoisting had been hiding the bug by deleting them first.Fix: null-check before unboxing, on both engines, alongside the
NaN/Infinitychecks already there. A category with no value is skipped rather than emitted as0, since0is a legitimate average and would be a wrong answer.OpenSearch routes every metric type through one method, so guarding it covers all of them. Elasticsearch split the same job across three overloads and guarding only one left an asymmetry, so the redundant
ValueCountAggregateoverload is gone — it extendsSingleMetricAggregateBase, sovalue_countnow binds to the guarded path like everything else. No behaviour change:value_countnever returns null.Worth noting for reviewers: this is the only change here that fixes a live 500 on
main, and it has no dependency on the rest of the PR.Also fixed in the same files
getEntityAttributeFieldsreturned the sharedcommonlist and appended to it in place. The workflow parses the config once and loops over every type, so each type inherited the attributes of every type before it, and which ones followed the salted order above.mappingFieldsentry, instead of an NPE.mappingFieldskeys are typed by the enum, so a misspelling fails at load rather than silently producing documents without their type-specific attributes.What changes for consumers
The catalog grows from ~277 records to ~1,082. This is union versus sum:
idexists on every type and was listed once, now once per type. No new information, correct attribution.Consumers must group by
entityType. Collate's drag-and-drop asset panel already does, and this fixes it there (topicgoes from 0 fields to 52). Its three flat-list pickers do not, and are handled in collate#6077.Filtered category charts stop drawing zero bars, because terms uses
min_doc_count: 1. The clearest case is a service Insights tab:healthy_data_assetsis requested with a service filter and today returns ~100 rows of which 99 are zero, so the widget reports another service's number over a 99 "day" window. It now returns the one service.Counts can move up. Terms samples per shard, and a category outside a shard's cutoff loses that shard's contribution. Re-ranking over the filtered population promotes exactly those marginal categories, so one can come back with a larger, more complete number. Counts move toward completeness; they are not asserted unchanged.
This is the pre-existing terms approximation getting smaller, not a new source of error. Elasticsearch's condition for an accurate
doc_countand accurate sub-aggregations is that the population a bucket is ranked on equals the population it counts. Before this change those two disagree by construction -- ranked on unfiltered documents, counted on filtered ones -- which is the regime in which shard truncation does the most damage. Hoisting makes them the same population again.What this does not fix
size(100)is unchanged. This changes which hundred categories you get, not how many.sum(k='size')chart still ranks by document count.groupBydimension keeps the same misalignment: it builds its ownterms(...).size(100)and is untouched whenever the x-axis is@timestamp.q='...'is deliberately not hoisted. Doing so would scope the denominator of every percentage chart and make them all read 100%.Tests
Each verified to fail against the bug it guards:
metricremoved from config.jsonmetricexpected: <[table, topic]> but was: <[table]>toUnmodifiableSet()commoncopy removedexpected: <[id, service]> but was: <[id, columns, service]>groupByonlyATermsAxisIsHoistableDouble.doubleValue()NPE, per engineThe 12 pre-existing aggregator tests pass unmodified. None of their fixtures sets a metric filter, which is what proves an unfiltered chart still produces a byte-identical request.
Greptile Summary
The PR centralizes the Data Insights asset-type contract and makes field-catalog generation deterministic. It also aligns categorical bucket selection with metric filters and safely skips null aggregate values.
Confidence Score: 5/5
The PR appears safe to merge.
No blocking failure remains.
Important Files Changed
Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart LR Schema[DataAssetType schema enum] --> Catalog[Chart field catalog types] Schema --> Ingestion[Ingested asset types] Mapping[Index mappings and live aliases] --> Ingestion Config[Per-type mapping fields] --> Streams[Data Insights documents] Ingestion --> Streams Streams --> Terms[Categorical terms axis] Filter[Metric filter] --> Wrapper[Filtered metric aggregation] Wrapper --> Terms Terms --> Results[Chart results] Results --> NullGuard[Skip null aggregate values]Reviews (23): Last reviewed commit: "fix(data-insights): rank a categorical a..." | Re-trigger Greptile
Context used (3)