From 33a02312d998b209a3facc4c8c85a4f7906ecda2 Mon Sep 17 00:00:00 2001 From: tryeverything24 <114252040+tryeverything24@users.noreply.github.com> Date: Sun, 26 Jul 2026 04:09:28 +0300 Subject: [PATCH] fix(ui): migrate the pre-rebrand analytics window preference via the legacyKey fallback app.analytics.tsx called useLocalStorage with no third legacyKey argument, unlike its rebrand siblings app.runs.tsx and app.workbench.tsx. The storage key was renamed from gittensory.analytics.windowDays to loopover.analytics.windowDays in the same rebrand commit (75450f1d5) that added the legacyKey migration elsewhere, so a returning user who had picked a 30d/90d window silently reverted to the 7-day default. Pass gittensory.analytics.windowDays as the legacy key: the hook reads it once when the new key is absent, adopts the value, and backfills the new key, exactly like the runs/workbench view preferences. Regenerates scripts/branding-drift-baseline.json (branding-drift:update) for the one intentional legacy-key reference this adds, so branding-drift:check stays green. Closes #8699 --- .../src/routes/app.analytics.test.tsx | 61 ++++++++++++++++++- apps/loopover-ui/src/routes/app.analytics.tsx | 3 + scripts/branding-drift-baseline.json | 1 + 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/apps/loopover-ui/src/routes/app.analytics.test.tsx b/apps/loopover-ui/src/routes/app.analytics.test.tsx index fa84322ecf..7b85c70958 100644 --- a/apps/loopover-ui/src/routes/app.analytics.test.tsx +++ b/apps/loopover-ui/src/routes/app.analytics.test.tsx @@ -1,5 +1,5 @@ -import { render, screen } from "@testing-library/react"; -import { describe, expect, it, vi } from "vitest"; +import { render, screen, waitFor } from "@testing-library/react"; +import { afterEach, describe, expect, it, vi } from "vitest"; // #6817: app.analytics.tsx's StateBoundary had no loadingSkeleton, falling through to the generic spinner // -- the same gap #6816 fixed for app.operator.tsx. @@ -42,3 +42,60 @@ describe("ProductAnalytics loading skeleton (#6817)", () => { expect(container.querySelectorAll(".animate-pulse").length).toBe(0); }); }); + +// #8699: the analytics window preference was renamed from "gittensory.analytics.windowDays" to +// "loopover.analytics.windowDays" in the rebrand (75450f1d5) without the one-time legacyKey fallback +// app.runs.tsx / app.workbench.tsx got, silently resetting returning users to the 7-day default. +describe("ProductAnalytics window preference rebrand migration (#8699)", () => { + afterEach(() => { + window.localStorage.clear(); + }); + + it("migrates a pre-rebrand gittensory window preference forward instead of resetting to 7d", async () => { + window.localStorage.setItem("gittensory.analytics.windowDays", "30"); + useApiResource.mockReturnValue({ + status: "ready", + data: { metrics: [{ label: "Active repos", value: "12", delta: "+2" }], noiseReduction: [] }, + error: null, + loadedAt: "2026-07-17T00:00:00.000Z", + reload: () => {}, + }); + + render(); + + // The toggle group renders once the preference has hydrated -- with the legacy value carried + // over, 30d is the selected window (not the 7-day default the user never chose). + const thirtyDay = await screen.findByRole("radio", { name: "30 day window" }); + await waitFor(() => expect(thirtyDay.getAttribute("data-state")).toBe("on")); + expect(screen.getByRole("radio", { name: "7 day window" }).getAttribute("data-state")).toBe( + "off", + ); + // The dashboard is fetched for the migrated window, not the default. + await waitFor(() => + expect(useApiResource).toHaveBeenLastCalledWith( + "/v1/app/operator-dashboard?days=30", + "Product analytics", + ), + ); + // Backfilled: the new key now holds the value directly (the hook migrates it forward on first read). + expect(window.localStorage.getItem("loopover.analytics.windowDays")).toBe("30"); + }); + + it("still uses the 7-day default when neither the new nor the legacy key is present", async () => { + useApiResource.mockReturnValue({ + status: "ready", + data: { metrics: [{ label: "Active repos", value: "12", delta: "+2" }], noiseReduction: [] }, + error: null, + loadedAt: "2026-07-17T00:00:00.000Z", + reload: () => {}, + }); + + render(); + + const sevenDay = await screen.findByRole("radio", { name: "7 day window" }); + await waitFor(() => expect(sevenDay.getAttribute("data-state")).toBe("on")); + // Nothing to migrate: no value is invented under either key. + expect(window.localStorage.getItem("loopover.analytics.windowDays")).toBeNull(); + expect(window.localStorage.getItem("gittensory.analytics.windowDays")).toBeNull(); + }); +}); diff --git a/apps/loopover-ui/src/routes/app.analytics.tsx b/apps/loopover-ui/src/routes/app.analytics.tsx index f852047a5b..c8513a2f6c 100644 --- a/apps/loopover-ui/src/routes/app.analytics.tsx +++ b/apps/loopover-ui/src/routes/app.analytics.tsx @@ -172,6 +172,9 @@ export function ProductAnalytics() { const [windowDays, setWindowDays, windowHydrated] = useLocalStorage( ANALYTICS_WINDOW_STORAGE_KEY, DEFAULT_ANALYTICS_WINDOW_DAYS, + // Pre-rebrand key (#8699): read once as a fallback and migrate forward, mirroring + // app.runs.tsx / app.workbench.tsx, so a returning user's window choice survives the rename. + "gittensory.analytics.windowDays", ); const selectedWindow = parseAnalyticsWindowDays(windowDays); const dashboard = useApiResource( diff --git a/scripts/branding-drift-baseline.json b/scripts/branding-drift-baseline.json index 1b116393fc..cdc06f68d7 100644 --- a/scripts/branding-drift-baseline.json +++ b/scripts/branding-drift-baseline.json @@ -1,5 +1,6 @@ { "apps/loopover-ui/src/components/site/api/try-it.tsx": 1, + "apps/loopover-ui/src/routes/app.analytics.tsx": 1, "apps/loopover-ui/src/routes/app.index.tsx": 1, "apps/loopover-ui/src/routes/app.runs.tsx": 1, "apps/loopover-ui/src/routes/app.workbench.tsx": 1,