Skip to content

feat: persist search history by locale#3231

Open
lemagnetic wants to merge 3 commits intofeat/multilanguagefrom
feat/search-history-by-locale
Open

feat: persist search history by locale#3231
lemagnetic wants to merge 3 commits intofeat/multilanguagefrom
feat/search-history-by-locale

Conversation

@lemagnetic
Copy link
Contributor

@lemagnetic lemagnetic commented Feb 25, 2026

What's the purpose of this pull request?

Stores with multiple locales shared one search history. This PR stores history per locale so each language has its own list.

How it works?

  • Same IndexedDB key fs::searchHistory, but value is now Record<locale, History[]> (e.g. { "en-US": [...], "pt-BR": [...] }).
  • useSearchHistory uses useSession().locale to read/write only the current locale. Public API unchanged.
  • Legacy array format is migrated on first load to the new shape (array → { defaultLocale: [...] }).

How to test it?

  1. Run the store (pnpm dev).
  2. In locale A: search a few terms, focus search again → history appears.
  3. Switch to locale B → history is empty (or only B’s terms).
  4. Search in B, focus → only B’s terms. Switch back to A → A’s history still there.
  5. (Optional) DevTools → Application → IndexedDB → fs::searchHistory → object with one key per locale.
image

Starters Deploy Preview

https://brandless-cma5xay4001f6dn4xjwato8b4-mw8252v1z-vtex-fs.vercel.app/it-IT
https://brandless-cma5xay4001f6dn4xjwato8b4-mw8252v1z-vtex-fs.vercel.app/pt-BR

References

Summary by CodeRabbit

  • New Features

    • Search history is now locale-aware: each language/locale keeps its own history and changes are isolated per locale.
    • Legacy array-based histories are migrated automatically to the locale-based format.
  • Behavior Improvements

    • History entries are deduplicated, newest-first, and capped to a maximum size.
    • Clearing history removes only the active locale’s entries.
  • Tests

    • Comprehensive tests added for locale-aware history, migration, deduplication, limits, and clearing behavior.

@lemagnetic lemagnetic requested a review from a team as a code owner February 25, 2026 21:43
@lemagnetic lemagnetic requested review from eduardoformiga and lariciamota and removed request for a team February 25, 2026 21:43
@lemagnetic lemagnetic added the enhancement New feature or request label Feb 25, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 25, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b1b46d7 and 0a9b889.

📒 Files selected for processing (1)
  • packages/core/src/sdk/search/useSearchHistory.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/core/src/sdk/search/useSearchHistory.ts

Walkthrough

The useSearchHistory hook now stores search histories per locale (Record<string, History[]>), adds migration from legacy array format, derives a deterministic locale with a config fallback, and updates add/clear operations to act on the active locale with deduplication and size enforcement while preserving other locales.

Changes

Cohort / File(s) Summary
Hook Implementation
packages/core/src/sdk/search/useSearchHistory.ts
Switches store shape from History[] to SearchHistoryByLocale (Record<string, History[]>). Adds DEFAULT_LOCALE, migrateSearchHistory, locale derivation from session/config, per-locale memoization, migration effect, and updates addToSearchHistory / clearSearchHistory to operate on the active locale with dedupe and max-size enforcement. Exports new types/constants and updates initial store declaration.
Test Suite
packages/core/test/sdk/search/useSearchHistory.test.ts
Adds Vitest tests covering reading per-locale histories, session-locale changes, add behavior (dedupe, front-insert, maxHistorySize), clear behavior (locale-scoped removal, preserve others, clear-to-empty), and migration from legacy array to Record format.

Sequence Diagram(s)

sequenceDiagram
    actor Session
    participant Hook as useSearchHistory
    participant Config
    participant Store as searchHistoryStore

    Note over Session,Config: Hook initializes
    Session->>Hook: provide session.locale
    Config->>Hook: provide defaultLocale & localization.enabled
    Hook->>Store: read value
    alt Store is legacy array
        Hook->>Hook: migrateSearchHistory(array) -> Record
        Hook->>Store: write migrated Record
    end

    Session->>Hook: addToSearchHistory(term)
    Hook->>Store: read current Record
    Hook->>Hook: update Record[currentLocale] (dedupe, limit)
    Hook->>Store: write updated Record

    Session->>Hook: clearSearchHistory()
    Hook->>Store: read current Record
    Hook->>Hook: remove currentLocale entry
    Hook->>Store: write updated Record (or empty object)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • eduardoformiga
  • lariciamota

Poem

✨ A history split by tongue and time,
Old arrays migrated, now kept in rhyme,
Each locale holds its newest quest —
Clean, distinct, and tidy at rest. 🎐

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: persist search history by locale' accurately and concisely summarizes the main change—converting search history storage to be locale-specific.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/search-history-by-locale

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codesandbox-ci
Copy link

codesandbox-ci bot commented Feb 25, 2026

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

@lemagnetic lemagnetic changed the title feat(search): persist search history by locale feat: persist search history by locale Feb 25, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/core/test/sdk/search/useSearchHistory.test.ts (1)

212-214: Prefer waitFor over microtask flushing for migration assertion.

Lines 212-214 rely on Promise.resolve() timing. Using waitFor on store shape is less brittle to scheduling changes.

🔧 Suggested patch
-import { act, renderHook } from '@testing-library/react'
+import { act, renderHook, waitFor } from '@testing-library/react'
@@
-      await act(async () => {
-        await Promise.resolve()
-      })
+      await waitFor(() => {
+        expect(Array.isArray(searchHistoryStore.read())).toBe(false)
+      })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/test/sdk/search/useSearchHistory.test.ts` around lines 212 -
214, The test currently flushes microtasks via act(async () => { await
Promise.resolve() }) which is brittle; replace that with testing-library's
waitFor to assert the expected store shape/state instead. In the
useSearchHistory test, remove the Promise.resolve() flush and wrap the assertion
that inspects the store (or the hook's returned state) inside await waitFor(()
=> { /* assert store shape or expected values from useSearchHistory */ }); this
will wait for the state to settle reliably and avoid timing flakiness.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/core/src/sdk/search/useSearchHistory.ts`:
- Around line 53-55: The current construction of newHistoryArray in
useSearchHistory adds newHistory then truncates with slice(0, maxHistorySize)
before deduplication, which can drop newer unique items; change the operation
order so you first build the full array ([newHistory, ...current]), run the
dedupe filter using the existing Set (set) to remove duplicates (.filter((item)
=> !set.has(item.term) && set.add(item.term))), and only then apply .slice(0,
maxHistorySize) to truncate; update the code around newHistoryArray in
useSearchHistory to perform filter then slice so maxHistorySize reflects unique
entries.
- Around line 24-30: migrateSearchHistory currently casts any plain object to
SearchHistoryByLocale without checking structure; update it to validate runtime
shape: in the object branch (migrateSearchHistory) iterate Object.entries(value)
and only accept keys whose values are arrays where every element is a valid
History (e.g., typeof item === 'object' && item !== null and has required
properties like 'query' and any other expected fields), building a new
SearchHistoryByLocale from only those valid entries; in the Array branch
validate each element before returning { [DEFAULT_LOCALE]: validatedArray } and
if validation fails return an empty object or { [DEFAULT_LOCALE]: [] } as a safe
fallback; stop using a blind type assertion and return the constructed validated
object instead.

---

Nitpick comments:
In `@packages/core/test/sdk/search/useSearchHistory.test.ts`:
- Around line 212-214: The test currently flushes microtasks via act(async () =>
{ await Promise.resolve() }) which is brittle; replace that with
testing-library's waitFor to assert the expected store shape/state instead. In
the useSearchHistory test, remove the Promise.resolve() flush and wrap the
assertion that inspects the store (or the hook's returned state) inside await
waitFor(() => { /* assert store shape or expected values from useSearchHistory
*/ }); this will wait for the state to settle reliably and avoid timing
flakiness.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 75d6bbc and b1b46d7.

📒 Files selected for processing (2)
  • packages/core/src/sdk/search/useSearchHistory.ts
  • packages/core/test/sdk/search/useSearchHistory.test.ts

Comment on lines +24 to +30
function migrateSearchHistory(value: unknown): SearchHistoryByLocale {
if (value && typeof value === 'object' && !Array.isArray(value)) {
return value as SearchHistoryByLocale
}
if (Array.isArray(value) && value.length > 0) {
return { [DEFAULT_LOCALE]: value as History[] }
}
Copy link

@coderabbitai coderabbitai bot Feb 25, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "useSearchHistory.ts" -type f

Repository: vtex/faststore

Length of output: 108


🏁 Script executed:

wc -l packages/core/src/sdk/search/useSearchHistory.ts

Repository: vtex/faststore

Length of output: 109


🏁 Script executed:

cat -n packages/core/src/sdk/search/useSearchHistory.ts

Repository: vtex/faststore

Length of output: 2548


Add validation to migration function for runtime safety.

Line 26 casts any object directly to SearchHistoryByLocale without checking structure. If IndexedDB persists malformed data (e.g., { "en-US": "not-an-array" }), code relying on locale values as arrays could fail. Validate each history entry before returning from migration.

🔧 Suggested patch
+function isHistory(value: unknown): value is History {
+  return (
+    typeof value === 'object' &&
+    value !== null &&
+    typeof (value as { term?: unknown }).term === 'string' &&
+    typeof (value as { path?: unknown }).path === 'string'
+  )
+}
+
+function normalizeHistory(value: unknown): History[] {
+  return Array.isArray(value) ? value.filter(isHistory) : []
+}
+
 function migrateSearchHistory(value: unknown): SearchHistoryByLocale {
-  if (value && typeof value === 'object' && !Array.isArray(value)) {
-    return value as SearchHistoryByLocale
-  }
-  if (Array.isArray(value) && value.length > 0) {
-    return { [DEFAULT_LOCALE]: value as History[] }
+  if (Array.isArray(value)) {
+    const normalized = normalizeHistory(value)
+    return normalized.length > 0 ? { [DEFAULT_LOCALE]: normalized } : {}
   }
-  return {}
+  if (!value || typeof value !== 'object') return {}
+
+  return Object.entries(value as Record<string, unknown>).reduce(
+    (acc, [locale, history]) => {
+      const normalized = normalizeHistory(history)
+      if (normalized.length > 0) acc[locale] = normalized
+      return acc
+    },
+    {} as SearchHistoryByLocale
+  )
 }

Per coding guidelines: "Ensure type safety and avoid type assertions when possible."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/sdk/search/useSearchHistory.ts` around lines 24 - 30,
migrateSearchHistory currently casts any plain object to SearchHistoryByLocale
without checking structure; update it to validate runtime shape: in the object
branch (migrateSearchHistory) iterate Object.entries(value) and only accept keys
whose values are arrays where every element is a valid History (e.g., typeof
item === 'object' && item !== null and has required properties like 'query' and
any other expected fields), building a new SearchHistoryByLocale from only those
valid entries; in the Array branch validate each element before returning {
[DEFAULT_LOCALE]: validatedArray } and if validation fails return an empty
object or { [DEFAULT_LOCALE]: [] } as a safe fallback; stop using a blind type
assertion and return the constructed validated object instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We're not adding runtime validation here on purpose.

  • We control all writes: The only code that persists to fs::searchHistory is in this hook (addToSearchHistory, clearSearchHistory, and the migration itself). Every write uses the typed History / SearchHistoryByLocale shape, and TypeScript enforces that at compile time.
  • Malformed data would only occur if someone changed the value manually (e.g. in DevTools → Application → IndexedDB) or due to external corruption. We don't consider that a supported scenario worth extra runtime checks for this internal store.

So we're leaving the migration as-is.

Copy link

Choose a reason for hiding this comment

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

@lemagnetic, understood. That's a reasonable tradeoff—since all persistence paths go through typed functions in this hook, TypeScript guards the shape at compile time. Leaving it as-is makes sense for an internal store where manual corruption isn't a supported scenario.


✏️ Learnings added
Learnt from: lemagnetic
Repo: vtex/faststore PR: 3231
File: packages/core/src/sdk/search/useSearchHistory.ts:24-30
Timestamp: 2026-02-26T14:03:22.212Z
Learning: In FastStore's SDK hooks (e.g., useSearchHistory), runtime validation of persisted store data is intentionally omitted when all writes are controlled by the same typed hook. TypeScript enforces shape at compile time, and manual corruption via DevTools/IndexedDB is not considered a supported scenario worth extra runtime checks.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

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

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant