mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
* fix(analytics): use state for runtime app version so PostHog gets the real value
`useAppVersion()` stored the fetched `/api/version` result in a `useRef`,
but ref writes do NOT trigger a re-render. The hook therefore kept
returning '0.0.0' forever and the downstream `useEffect` that calls
`client.register({ app_version, ui_version })` never re-ran with the
real version. PostHog dashboards then showed `app_version=0.0.0` and
`ui_version=0.0.0` on every event ever shipped from the web client.
Switching to `useState` lets the resolved version flow through React's
render cycle so the register-on-change effect picks it up. The boot
placeholder still ships as '0.0.0' for the first events before the
fetch resolves (we don't re-emit those), but every event after init
now carries the real daemon-pinned version.
Adds a red-spec at apps/web/tests/analytics-app-version.test.tsx that
went red on the `useRef` shape (`expected '0.0.0' to be '1.2.3'`) and
green on the `useState` shape, so a future refactor can't silently
regress it.
* feat(analytics): wire media providers click events + lock run_finished error_code invariant
Two analytics gaps shipped together because both came out of the same
PostHog spot-check after PR #2390 landed:
1. Settings → Media providers (CSV row "client_type=desktop / mason /
media_providers") wasn't emitting any ui_click events. The contract
type `SettingsMediaProvidersClickProps` and helper
`trackSettingsMediaProvidersClick` were defined but no call site
used them, so the dashboard showed zero traffic on every element.
Added the four v2 elements:
- `reload` on the "Reload from daemon" button
- `key_input` on every per-provider API key field (onFocus, mirrors
the BYOK key field pattern in this same dialog)
- `url_input` on every per-provider base-URL field
- `clear` on each row's Clear button (fires before the confirm
dialog so the intent signal is recorded even if the user backs
out)
Each event carries `providers_id` (provider.id) and `is_configured`
(truthy when the row has a stored entry).
2. `run_finished` with `result=failed` was reported as missing
`error_code` on PostHog. Audited every failure path: the daemon's
`child.on('close', ...)` handler has several branches that call
`runs.finish('failed', code, signal)` directly without first
emitting an SSE `error` event (ACP fatal, agentStreamError fall
through, child close without diagnostic), leaving
`run.errorCode === null` in the status body. The existing fallback
in `server.ts` already derives `AGENT_SIGNAL_*` / `AGENT_EXIT_*` /
`AGENT_TERMINATED_UNKNOWN` from `signal` / `exitCode` for those
cases, so the wire emission should never blank out — but the logic
was inline and had no unit coverage.
Extracted the result/error_code derivation into
`apps/daemon/src/run-result.ts` and added 12 unit tests covering:
- explicit errorCode forwarding
- signal-only failures
- exit-code-only failures
- clean (code=0) failures (ACP fatal shape)
- cancelled runs (with and without stamped code)
- empty-string errorCode defensive case
- status→result mapping for succeeded/canceled/failed/unknown
All 12 pass — confirming the invariant "result=failed always
carries error_code" holds for every failure shape the daemon
produces. The refactor pins that invariant so a future change
loses test coverage rather than silently regressing on PostHog.
If `error_code` still looks empty on a live event, share the
PostHog event JSON + the agent id and I'll dig further — at this
point the daemon emission itself is exercised end-to-end.
75 lines
2.8 KiB
TypeScript
75 lines
2.8 KiB
TypeScript
// @vitest-environment jsdom
|
|
//
|
|
// Regression test for "every PostHog event ships with app_version='0.0.0'".
|
|
//
|
|
// `useAppVersion()` reads /api/version at runtime so the same web bundle
|
|
// reports the daemon-pinned version even when running against a newer or
|
|
// older daemon during dev. The previous implementation stored the fetched
|
|
// version in a `useRef`, which silently broke the contract: ref writes do
|
|
// NOT trigger a re-render, so the hook kept returning '0.0.0' forever and
|
|
// every downstream useEffect that depended on `appVersion`
|
|
// (`client.register({ app_version })` in particular) never re-ran with
|
|
// the real version. PostHog dashboards then showed `app_version=0.0.0`
|
|
// on every event.
|
|
//
|
|
// This test goes red on the `useRef` version and green on the `useState`
|
|
// version: after the mocked /api/version resolves, the hook must return
|
|
// the fetched value, not the boot placeholder.
|
|
|
|
import { act, renderHook, waitFor } from '@testing-library/react';
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import { useAppVersion } from '../src/analytics/provider';
|
|
|
|
describe('useAppVersion', () => {
|
|
const originalFetch = globalThis.fetch;
|
|
|
|
beforeEach(() => {
|
|
globalThis.fetch = vi.fn(async (input: RequestInfo | URL) => {
|
|
const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
|
|
if (url.endsWith('/api/version')) {
|
|
return new Response(
|
|
JSON.stringify({
|
|
version: {
|
|
version: '1.2.3',
|
|
channel: 'development',
|
|
packaged: false,
|
|
platform: 'darwin',
|
|
arch: 'arm64',
|
|
},
|
|
}),
|
|
{ status: 200, headers: { 'content-type': 'application/json' } },
|
|
);
|
|
}
|
|
return new Response('not found', { status: 404 });
|
|
}) as unknown as typeof fetch;
|
|
});
|
|
|
|
afterEach(() => {
|
|
globalThis.fetch = originalFetch;
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it('boots to the 0.0.0 placeholder before the fetch resolves', () => {
|
|
const { result } = renderHook(() => useAppVersion());
|
|
expect(result.current).toBe('0.0.0');
|
|
});
|
|
|
|
it('updates to the fetched version once /api/version resolves', async () => {
|
|
const { result } = renderHook(() => useAppVersion());
|
|
await waitFor(() => {
|
|
expect(result.current).toBe('1.2.3');
|
|
});
|
|
});
|
|
|
|
it('keeps the 0.0.0 placeholder when the fetch fails', async () => {
|
|
globalThis.fetch = vi.fn(async () => new Response('boom', { status: 500 })) as unknown as typeof fetch;
|
|
const { result } = renderHook(() => useAppVersion());
|
|
// Let any pending microtasks settle so a buggy implementation has the
|
|
// same opportunity to "succeed" with stale data as the happy path.
|
|
await act(async () => {
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
});
|
|
expect(result.current).toBe('0.0.0');
|
|
});
|
|
});
|