Skip to content

feat: [ENG-2621] serve analytics disclosure markdown via daemon + canonicalise privacy URL#752

Open
ncnthien wants to merge 2 commits into
proj/analytics-system-tool-modefrom
feat/ENG-2621
Open

feat: [ENG-2621] serve analytics disclosure markdown via daemon + canonicalise privacy URL#752
ncnthien wants to merge 2 commits into
proj/analytics-system-tool-modefrom
feat/ENG-2621

Conversation

@ncnthien
Copy link
Copy Markdown
Collaborator

@ncnthien ncnthien commented Jun 1, 2026

Summary

Two related changes that finish the wiring between `src/shared/assets/analytics-disclosure.md` and the local web UI's Privacy tab.

1. Privacy URL canonicalised (`e8de448a`)

`PRIVACY_POLICY_URL` in `src/shared/constants/privacy.ts` now points at the published `https://www.byterover.dev/services/privacy\` page. Drops the stale "M1.5 placeholder" comment.

2. Daemon-served disclosure markdown (`94b1cae7`)

The webui Privacy panel previously rendered a hardcoded 2-column icon grid of Lorem-ipsum bodies, which drifted out of sync once the real PM/legal copy landed in the markdown file (commit `125b5c44`).

  • New transport event `analytics:getDisclosure` + Zod response schema (`AnalyticsDisclosureResponseSchema`).
  • New `AnalyticsDisclosureHandler` wraps `loadAnalyticsDisclosureText()` (already used by the CLI consent prompt) and exposes it over the transport. Wired into `feature-handlers.ts` next to the other Analytics handlers.
  • Webui `useGetAnalyticsDisclosure` TanStack Query hook + `DisclosureDetails` rewrite that renders the markdown via the existing `MarkdownView` from the contexts feature (handles lists, code blocks, links).
  • Deletes the obsolete `ANALYTICS_DISCLOSURE_SECTIONS` array, the icon grid layout, and the duplicate `ANALYTICS_PRIVACY_URL` constant. `analytics-panel.tsx` now consumes `PRIVACY_POLICY_URL` directly from `src/shared/constants/privacy.ts`.
  • Test coverage: 3 handler tests (`analytics-disclosure-handler.test.ts`) + 3 webui API hook tests (`get-analytics-disclosure.test.ts`); the obsolete constants test file is removed.

Single source of truth is now the `.md` file — PM/legal edits propagate to both the CLI prompt and the webui Privacy panel with no code changes.

Test plan

  • `npm run dev:ui` → `brv webui` → Configuration → Privacy → "What data will be collected?" section renders the real markdown (5 H2 sections, bullet list, code block for the CLI command, privacy link)
  • Refocus the tab → disclosure refreshes (TanStack Query default + override on the globalConfig query)
  • Click the footer privacy link → opens `https://www.byterover.dev/services/privacy\` in a new tab
  • Toggle on/off still routes to the enable/disable confirm dialogs
  • `./bin/dev.js settings set analytics.share true` shows the same markdown text in the CLI prompt
  • `npm run lint`, `npm run typecheck`, `npm test` green

ncnthien added 2 commits June 1, 2026 18:01
Replace the docs.byterover.dev placeholder with the canonical
https://www.byterover.dev/services/privacy URL now that the public page
has been published. Drops the stale "placeholder until M1.5" comment —
the URL is no longer a placeholder.
…ender it in the webui

Single source of truth for the disclosure copy lives in
src/shared/assets/analytics-disclosure.md and was already consumed by the
CLI consent prompt via loadAnalyticsDisclosureText(). The webui's Privacy
panel had a separate hardcoded copy that drifted out of sync once the real
PM/legal copy landed.

- Add `analytics:getDisclosure` transport event + Zod response schema.
- New AnalyticsDisclosureHandler wraps loadAnalyticsDisclosureText() and
  exposes it over the transport; wired into feature-handlers.ts alongside
  the other AnalyticsHandler family.
- Webui `useGetAnalyticsDisclosure` query hook + render via the existing
  MarkdownView from the contexts feature so lists, code blocks, and links
  in the markdown render properly.
- Delete the now-obsolete ANALYTICS_DISCLOSURE_SECTIONS / icon grid and
  the ANALYTICS_PRIVACY_URL duplicate; analytics-panel.tsx now consumes
  PRIVACY_POLICY_URL directly from src/shared/constants/privacy.ts.
- Drop the constants test file along with the hardcoded data it covered.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 1, 2026

Claude finished @ncnthien's task in 2m 52s —— View job


Review summary

Overall score: 4 / 5 — Good Outside-In design (consumer-driven transport event, shared loader, sensible DI, solid unit tests). One blocking inconsistency between this PR's two commits, plus a handful of small follow-ups. Inline comments posted on the relevant lines.


Findings

🔴 blocking — privacy URLs disagree inside the same panel
After both commits land, the Configuration → Privacy panel will simultaneously show:

  • Footer link: https://www.byterover.dev/services/privacy (new PRIVACY_POLICY_URL)
  • Inside the rendered markdown body: https://byterover.dev/privacy (the placeholder still in src/shared/assets/analytics-disclosure.md:56)

The CLI brv settings set analytics.share true prompt prints the same stale URL. This directly contradicts the PR's stated goal of canonicalising the URL with a single source of truth. I couldn't post an inline comment on the markdown because it isn't in this PR's diff — fix by updating src/shared/assets/analytics-disclosure.md line 56 to match PRIVACY_POLICY_URL, or by interpolating the constant into the markdown server-side in AnalyticsDisclosureHandler. Fix this →

🟡 suggestion — analytics-panel.tsx:111 hardcodes the display label byterover.dev/services/privacy next to href={PRIVACY_POLICY_URL}. Centralising the URL doesn't help much if the label can still drift. Posted inline.

🟡 nitpick — PR description claim doesn't match code. Test plan says "Refocus the tab → disclosure refreshes (TanStack Query default + override on the globalConfig query)", but useGetAnalyticsDisclosure inherits refetchOnWindowFocus: false from the default config and doesn't override it. The disclosure won't refetch on focus. Bundled-asset markdown probably shouldn't refetch, so consider staleTime: Infinity + fixing the description. Posted inline.

🟡 nitpick — AnalyticsDisclosureResponseSchema declared but never safeParse'd. The schema enforces markdown: z.string().min(1), but nothing on the wire actually checks it. If loadDisclosure() ever returns "", the webui silently renders an empty <MarkdownView/> instead of hitting the error-state UI. Posted inline.


What's good

  • Outside-In: the panel drove the new transport event, shared loader, single source of truth (modulo the URL drift above).
  • DI in AnalyticsDisclosureHandler (optional loadDisclosure) keeps the unit test fast and in-memory.
  • Three handler tests + three webui hook tests cover success, error, and "not connected" paths.
  • Wired correctly into feature-handlers.ts:306 next to the other analytics handlers; barrel export updated; obsolete ANALYTICS_DISCLOSURE_SECTIONS / ANALYTICS_PRIVACY_URL fully removed (verified — no stragglers).
  • The webui no longer maintains a parallel copy of legal copy; PM/legal edits to the .md propagate.

  • Read changed files
  • Verify transport event wiring
  • Check handler implementation + tests
  • Review webui consumption (API hook + DisclosureDetails)
  • Validate test coverage
  • Post inline findings + summary
    · branch feat/ENG-2621

Comment on lines 104 to 112
<a
className="text-foreground/80 hover:text-foreground inline-flex items-center gap-2 border-t px-5 py-3 text-sm transition-colors"
href={ANALYTICS_PRIVACY_URL}
href={PRIVACY_POLICY_URL}
rel="noopener noreferrer"
target="_blank"
>
<ExternalLink className="size-3.5 text-primary" />
<span className="text-primary">docs.byterover.dev/privacy</span>
<span className="text-primary">byterover.dev/services/privacy</span>
</a>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion (non-blocking): the visible label byterover.dev/services/privacy is hardcoded next to href={PRIVACY_POLICY_URL}. The whole point of e8de448 is that there is now one canonical URL constant — but the label here can still drift independently. Consider deriving the label from PRIVACY_POLICY_URL (e.g. PRIVACY_POLICY_URL.replace(/^https?:\/\/(www\.)?/, '')) so a future URL change updates both the link and the text in one place.

Comment on lines +17 to +21
export const getAnalyticsDisclosureQueryOptions = () =>
queryOptions({
queryFn: getAnalyticsDisclosure,
queryKey: ['analyticsDisclosure'],
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nitpick (non-blocking): the PR description claims the webui will refetch the disclosure on tab refocus ("TanStack Query default + override on the globalConfig query"), but the global queryConfig in src/webui/lib/react-query.ts has refetchOnWindowFocus: false, and this hook doesn't override it (unlike useGetGlobalConfig, which sets refetchOnWindowFocus: true). So in practice the disclosure won't auto-refresh on focus — which is actually fine since the markdown is bundled with the CLI binary, but the PR description's test-plan bullet is misleading. Consider either:

  1. Adding staleTime: Infinity here to make the bundled-asset semantics explicit (and avoid a re-request every time the component remounts), or
  2. Just updating the PR description so it doesn't claim a refetch behavior the code doesn't implement.

Comment on lines +30 to +35
public setup(): void {
this.transport.onRequest<void, AnalyticsDisclosureResponse>(AnalyticsEvents.GET_DISCLOSURE, async () => {
const markdown = await this.loadDisclosure()
return {markdown}
})
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nitpick (non-blocking): AnalyticsDisclosureResponseSchema is declared in analytics-events.ts with markdown: z.string().min(1) but is only used here for type inference, never safeParse'd on the response. Sister handler AnalyticsListHandler validates inbound payloads with AnalyticsListRequestSchema.safeParse; this one has no inbound payload to validate. If loadDisclosure() ever returns "" (truncated read, bad bundle), the wire response will technically violate the .min(1) schema and the webui will silently render an empty <MarkdownView/>. Cheap defense: if (!markdown) throw new Error('disclosure missing') — keeps the failure mode visible (the existing error-state UI in DisclosureDetails) instead of "blank panel, no toast." Not blocking since the bundled asset is checked into git; flagging because the test on line 27-42 of the handler test already establishes the propagate-errors contract you'd be leaning on.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant