From 449e2d2dda9cc610dfb7278641a6fe26679716a5 Mon Sep 17 00:00:00 2001 From: Prabhat Sharma Date: Sun, 2 Aug 2026 17:05:27 -0700 Subject: [PATCH] refactor(editor): rename useFieldValueStore, and write down what it captures I claimed field values could only ever exist for logs streams, and repeated it after being questioned. Wrong twice over: the sidebar path captureFromValuesApi is reached from logs, traces AND pipeline, and captureFromSearchHits captures under whichever stream type the Logs page searched -- its selector covers metrics and traces. Proved by searching cache_hit_ratio in the Logs UI, after which the store held 21 metrics keys and the SLO form resolved environment -> [development, staging]. Two names made that easy to get wrong, so both are fixed rather than just apologised for: useFieldValueStore.ts -> fieldValueStore.ts It is not a composable -- plain functions over IndexedDB, no reactive state -- and the false `use` prefix left it one character from useFieldValuesStream, which IS a composable and does something else entirely. Dropping the prefix also puts it next to its sibling fieldValueDB.ts, where it belongs. The header now names BOTH writers, states that neither is tied to a stream type, and spells out the trap directly: composables/useLogs/ is the Logs PAGE, not the logs stream TYPE. StreamContext.streamType says the same at the field level -- part of the key, never a filter on what may be captured. The rule worth remembering: values come from what has been SEARCHED or EXPANDED. A stream nobody has looked at has none. That is a cold cache, not a missing capability -- which is exactly the distinction I failed to make, off one grep. Mechanical rename across 10 files including the vi.mock paths. 140 passing in the touched specs; type-check, prettier clean; the 13 eslint warnings in IndexList.vue are pre-existing (identical count with this change stashed). --- .../alerts/QueryEditorDialog.spec.ts | 2 +- .../alerts/steps/QueryConfig.spec.ts | 2 +- .../steps/AnomalyDetectionConfig.spec.ts | 2 +- ...eStore.spec.ts => fieldValueStore.spec.ts} | 2 +- ...eFieldValueStore.ts => fieldValueStore.ts} | 26 ++++++++++++++++--- web/src/composables/useFieldValuesStream.ts | 2 +- .../composables/useLogs/useStreamFields.ts | 2 +- .../useSuggestions.serverCatalog.spec.ts | 2 +- web/src/composables/useSuggestions.spec.ts | 4 +-- web/src/composables/useSuggestions.ts | 2 +- web/src/plugins/logs/IndexList.vue | 2 +- 11 files changed, 33 insertions(+), 15 deletions(-) rename web/src/composables/{useFieldValueStore.spec.ts => fieldValueStore.spec.ts} (99%) rename web/src/composables/{useFieldValueStore.ts => fieldValueStore.ts} (86%) diff --git a/web/src/components/alerts/QueryEditorDialog.spec.ts b/web/src/components/alerts/QueryEditorDialog.spec.ts index cd5705efa5..9505081550 100644 --- a/web/src/components/alerts/QueryEditorDialog.spec.ts +++ b/web/src/components/alerts/QueryEditorDialog.spec.ts @@ -634,7 +634,7 @@ describe("QueryEditorDialog - N1 context keywords reach the editor", () => { // Stored field values for the N1 value-context probe above. Only useSuggestions // consumes this module, so mocking it does not affect the rest of the suite. -vi.mock("@/composables/useFieldValueStore", () => ({ +vi.mock("@/composables/fieldValueStore", () => ({ getFieldValuesForSuggestion: vi.fn().mockResolvedValue(["error", "warn"]), })); diff --git a/web/src/components/alerts/steps/QueryConfig.spec.ts b/web/src/components/alerts/steps/QueryConfig.spec.ts index 4dcd6dfa66..897a71a6b2 100644 --- a/web/src/components/alerts/steps/QueryConfig.spec.ts +++ b/web/src/components/alerts/steps/QueryConfig.spec.ts @@ -2128,6 +2128,6 @@ describe("QueryConfig.vue", () => { // Stored field values for the N1 value-context probe above. Only useSuggestions // consumes this module, so mocking it does not affect the rest of the suite. -vi.mock("@/composables/useFieldValueStore", () => ({ +vi.mock("@/composables/fieldValueStore", () => ({ getFieldValuesForSuggestion: vi.fn().mockResolvedValue(["error", "warn"]), })); diff --git a/web/src/components/anomaly_detection/steps/AnomalyDetectionConfig.spec.ts b/web/src/components/anomaly_detection/steps/AnomalyDetectionConfig.spec.ts index 070f945b0a..3e74643332 100644 --- a/web/src/components/anomaly_detection/steps/AnomalyDetectionConfig.spec.ts +++ b/web/src/components/anomaly_detection/steps/AnomalyDetectionConfig.spec.ts @@ -55,7 +55,7 @@ vi.mock("@/services/stream", () => ({ const { getFieldValuesForSuggestion } = vi.hoisted(() => ({ getFieldValuesForSuggestion: vi.fn(async () => ["ERROR", "INFO"]), })); -vi.mock("@/composables/useFieldValueStore", () => ({ getFieldValuesForSuggestion })); +vi.mock("@/composables/fieldValueStore", () => ({ getFieldValuesForSuggestion })); vi.mock("@/components/dashboards/PanelSchemaRenderer.vue", () => ({ default: { template: '
' }, diff --git a/web/src/composables/useFieldValueStore.spec.ts b/web/src/composables/fieldValueStore.spec.ts similarity index 99% rename from web/src/composables/useFieldValueStore.spec.ts rename to web/src/composables/fieldValueStore.spec.ts index a4b53ea0e4..1e4f496a08 100644 --- a/web/src/composables/useFieldValueStore.spec.ts +++ b/web/src/composables/fieldValueStore.spec.ts @@ -23,7 +23,7 @@ import { captureFromSearchHits, captureFromValuesApi, getFieldValuesForSuggestion, -} from "./useFieldValueStore"; +} from "./fieldValueStore"; const ctx = { org: "myorg", streamType: "logs", streamName: "http_logs" }; diff --git a/web/src/composables/useFieldValueStore.ts b/web/src/composables/fieldValueStore.ts similarity index 86% rename from web/src/composables/useFieldValueStore.ts rename to web/src/composables/fieldValueStore.ts index 263eec3723..311f0e7129 100644 --- a/web/src/composables/useFieldValueStore.ts +++ b/web/src/composables/fieldValueStore.ts @@ -14,9 +14,22 @@ // along with this program. If not, see . /** - * Composable that orchestrates field value capture from both data sources: - * 1. Values API — field expansion in FieldList - * 2. Search result hits — Run Query results + * Persisted field VALUES, for autocomplete. Not a composable — plain functions + * over IndexedDB, which is why it carries no `use` prefix and sits beside + * fieldValueDB.ts. (It was `useFieldValueStore`, one character from the real + * composable `useFieldValuesStream`, and the two were mistaken for each other.) + * + * THE ONLY TWO WRITERS, and neither is tied to a stream type: + * 1. captureFromValuesApi — expanding a field in the sidebar. Reached from + * logs, traces and pipeline, via useFieldValuesStream. + * 2. captureFromSearchHits — Run Query results. Its one caller lives under + * composables/useLogs/, which is the Logs PAGE, not the logs stream TYPE: + * it captures under whichever type was searched, and that page's stream + * selector covers metrics and traces too. + * + * So the rule is "values come from what has been SEARCHED or EXPANDED", never + * "values are logs-only". A stream nobody has looked at yet simply has none; + * that is a cold cache, not a missing capability. * * All IndexedDB writes are scheduled via requestIdleCallback so they have * zero impact on main-thread rendering. Reads use an in-memory cache @@ -52,7 +65,12 @@ const READ_CACHE_MAX_ENTRIES = 500; export interface StreamContext { org: string; - streamType: string; // 'logs' | 'metrics' | 'traces' + /** + * Whichever type the caller searched or expanded — all three are stored and + * read back the same way. Part of the key, never a filter on what may be + * captured. + */ + streamType: "logs" | "metrics" | "traces" | (string & {}); streamName: string; } diff --git a/web/src/composables/useFieldValuesStream.ts b/web/src/composables/useFieldValuesStream.ts index b337cdb401..8715773ec9 100644 --- a/web/src/composables/useFieldValuesStream.ts +++ b/web/src/composables/useFieldValuesStream.ts @@ -17,7 +17,7 @@ import { ref, type Ref } from "vue"; import store from "@/stores"; import { generateTraceContext } from "@/utils/zincutils"; import useHttpStreaming from "@/composables/useStreamingSearch"; -import { captureFromValuesApi } from "@/composables/useFieldValueStore"; +import { captureFromValuesApi } from "@/composables/fieldValueStore"; export interface FieldValueEntry { key: string; diff --git a/web/src/composables/useLogs/useStreamFields.ts b/web/src/composables/useLogs/useStreamFields.ts index b28bfe6133..b266f4bbcb 100644 --- a/web/src/composables/useLogs/useStreamFields.ts +++ b/web/src/composables/useLogs/useStreamFields.ts @@ -23,7 +23,7 @@ import config from "@/aws-exports"; import { searchState } from "@/composables/useLogs/searchState"; import useStreams from "@/composables/useStreams"; import useSqlSuggestions from "@/composables/useSuggestions"; -import { captureFromSearchHits } from "@/composables/useFieldValueStore"; +import { captureFromSearchHits } from "@/composables/fieldValueStore"; import { useLocalLogFilterField, diff --git a/web/src/composables/useSuggestions.serverCatalog.spec.ts b/web/src/composables/useSuggestions.serverCatalog.spec.ts index 59da9eb325..bdeca14069 100644 --- a/web/src/composables/useSuggestions.serverCatalog.spec.ts +++ b/web/src/composables/useSuggestions.serverCatalog.spec.ts @@ -28,7 +28,7 @@ vi.mock("vuex", async (importOriginal) => { return { ...actual, useStore: vi.fn(() => mockStore) }; }); -vi.mock("@/composables/useFieldValueStore", () => ({ +vi.mock("@/composables/fieldValueStore", () => ({ getFieldValuesForSuggestion: vi.fn().mockResolvedValue([]), })); diff --git a/web/src/composables/useSuggestions.spec.ts b/web/src/composables/useSuggestions.spec.ts index 6724cce418..cede3cfde3 100644 --- a/web/src/composables/useSuggestions.spec.ts +++ b/web/src/composables/useSuggestions.spec.ts @@ -12,7 +12,7 @@ vi.mock("vuex", async (importOriginal) => { }); // ─── Mock IDB so no real storage is touched ─────────────────────────────────── -vi.mock("@/composables/useFieldValueStore", () => ({ +vi.mock("@/composables/fieldValueStore", () => ({ getFieldValuesForSuggestion: vi.fn().mockResolvedValue([]), })); @@ -23,7 +23,7 @@ vi.mock("@/services/query_functions", () => ({ default: { list: vi.fn().mockResolvedValue({ data: { list: [] } }) }, })); -import { getFieldValuesForSuggestion } from "@/composables/useFieldValueStore"; +import { getFieldValuesForSuggestion } from "@/composables/fieldValueStore"; import useSqlSuggestions from "./useSuggestions"; // ─── helper: build composable with common defaults ──────────────────────────── diff --git a/web/src/composables/useSuggestions.ts b/web/src/composables/useSuggestions.ts index 44c0ce629b..2c65dd9302 100644 --- a/web/src/composables/useSuggestions.ts +++ b/web/src/composables/useSuggestions.ts @@ -1,6 +1,6 @@ import { ref, computed } from "vue"; import { useStore } from "vuex"; -import { getFieldValuesForSuggestion } from "@/composables/useFieldValueStore"; +import { getFieldValuesForSuggestion } from "@/composables/fieldValueStore"; import { SQL_KEYWORDS, SQL_CLAUSE_KEYWORDS, diff --git a/web/src/plugins/logs/IndexList.vue b/web/src/plugins/logs/IndexList.vue index 9b882aa9bd..c9ef3afd9d 100644 --- a/web/src/plugins/logs/IndexList.vue +++ b/web/src/plugins/logs/IndexList.vue @@ -294,7 +294,7 @@ import OSelect from "@/lib/forms/Select/OSelect.vue"; import type { SelectModelValue } from "@/lib/forms/Select/OSelect.types"; import OSkeleton from "@/lib/feedback/Skeleton/OSkeleton.vue"; import OEmptyState from "@/lib/core/EmptyState/OEmptyState.vue"; -import { captureFromValuesApi } from "@/composables/useFieldValueStore"; +import { captureFromValuesApi } from "@/composables/fieldValueStore"; import { saveLogsStreamType, saveLogsStream } from "@/utils/streamPersist"; import { quoteSqlIdentifierIfNeeded } from "@/utils/query/sqlIdentifiers"; import { toast } from "@/lib/feedback/Toast/useToast";