Files
open-design/apps/web/tests/observability/experience-diagnostics.test.ts
Ray XiandGitHub 282416ccd8 feat(diagnostics): automatically collect consented failure evidence (OPEND-3397) (#8357)
* feat(diagnostics): persist and upload consented Agent fault evidence

* feat(diagnostics): capture cancellation and delivery experience failures

* feat(diagnostics): collect visible frontend failures through consented relay

* fix(web): reset cached analytics before diagnostic test mocks

Generated-By: looper 0.13.0 (runner=fixer, agent=codex)

* fix(web): reset template picker before it becomes interactive

* test(web): await campaign presentation before retry polling
2026-09-23 11:12:53 +00:00

76 lines
4.7 KiB
TypeScript

// @vitest-environment jsdom
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
import { renderHook } from '@testing-library/react';
import { reportExperienceEvent, reportProjectFailure, resetExperienceDiagnosticsForTests } from '../../src/observability/experience-diagnostics';
import { useExperienceError } from '../../src/observability/use-experience-error';
import { trackArtifactExportResult, trackRunFailedToastSurfaceView } from '../../src/analytics/events';
import { reportSafetyEvent } from '../../src/analytics/error-tracking';
const fetcher = vi.fn();
beforeEach(() => {
vi.useFakeTimers(); resetExperienceDiagnosticsForTests();
fetcher.mockReset().mockResolvedValue({ ok: true, status: 200 }); vi.stubGlobal('fetch', fetcher);
});
afterEach(() => { resetExperienceDiagnosticsForTests(); vi.useRealTimers(); vi.unstubAllGlobals(); });
const body = (index = 0) => JSON.parse(fetcher.mock.calls[index]![1].body);
it('captures an error card without PostHog and deduplicates repeated displays of the same run', async () => {
const props = { element: 'run_failed_toast', run_id: 'run-1', project_id: 'project-1', error_code: 'FAILED', message: 'SECRET' };
trackRunFailedToastSurfaceView(vi.fn(), props as never);
reportExperienceEvent('surface_view', props);
expect(fetcher).toHaveBeenCalledTimes(1);
expect(body().properties).toMatchObject({ runId: 'run-1', category: 'visible_error', errorCode: 'FAILED' });
expect(JSON.stringify(body())).not.toContain('SECRET');
expect(fetcher.mock.calls[0]![0]).toBe('/api/observability/event');
});
it.each(['client_white_screen', 'client_preview_runtime_error', 'client_preview_resource_error', 'client_resource_error', 'client_run_stuck'])('bridges existing %s independently of safety ingestion setup', (name) => {
reportSafetyEvent(name, { run_id: 'r', error_message: 'SECRET', resource_url: 'https://secret.test' });
expect(body().properties.category).toBe('runtime_failure');
expect(JSON.stringify(body())).not.toContain('secret');
expect(JSON.stringify(body())).not.toContain('SECRET');
});
it('deduplicates project failures across surfaces and reports a newer run separately', () => {
const project = { id: 'p', status: { value: 'incomplete', runId: 'r' } };
reportProjectFailure(project, 'workspace_tabs'); reportProjectFailure(project, 'recent_projects');
reportProjectFailure({ ...project, status: { value: 'failed', runId: 'r2' } }, 'workspace_tabs');
expect(fetcher).toHaveBeenCalledTimes(2);
});
it('ignores successful/awaiting-input projects and canceled or successful exports', () => {
for (const value of ['succeeded', 'awaiting_input', 'running']) reportProjectFailure({ id: 'p', status: { value } }, 'tabs');
for (const result of ['success', 'cancelled']) trackArtifactExportResult(vi.fn(), { result } as never);
expect(fetcher).not.toHaveBeenCalled();
});
it('captures typed failed export and local load error without transmitting error text', () => {
trackArtifactExportResult(vi.fn(), { result: 'failed', error_code: 'export_failed', project_id: 'p' } as never);
const hook = renderHook(({ error }) => useExperienceError(error, 'file_source_load', 'p'), { initialProps: { error: '' } });
hook.rerender({ error: 'private filename and content' });
expect(fetcher).toHaveBeenCalledTimes(2);
expect(JSON.stringify(body(1))).not.toContain('private');
hook.unmount();
});
it('captures no-run/no-code cards and start-blocked events', () => {
reportExperienceEvent('surface_view', { element: 'run_failed_toast' });
reportExperienceEvent('surface_view', { element: 'run_start_blocked', block_reason: 'auth_required' });
expect(fetcher).toHaveBeenCalledTimes(2);
});
it('retries delivery with the same identity, finitely, without recursive reporting', async () => {
fetcher.mockRejectedValue(new Error('offline'));
reportExperienceEvent('client_white_screen', {});
await vi.runAllTimersAsync();
expect(fetcher).toHaveBeenCalledTimes(3);
expect(new Set(fetcher.mock.calls.map((_, i) => body(i).properties.occurrenceId)).size).toBe(1);
});
it('does not retry an opt-out acknowledgement or a rejected payload', async () => {
fetcher.mockResolvedValue({ ok: true, status: 200 });
reportExperienceEvent('client_white_screen', {});
await vi.runAllTimersAsync(); expect(fetcher).toHaveBeenCalledTimes(1);
fetcher.mockResolvedValue({ ok: false, status: 400 });
reportExperienceEvent('client_run_stuck', {});
await vi.runAllTimersAsync(); expect(fetcher).toHaveBeenCalledTimes(2);
});
it('captures another failed operation after its duplicate-display window', async () => {
reportExperienceEvent('artifact_edit_result', { result: 'failed' });
await vi.advanceTimersByTimeAsync(11_000);
reportExperienceEvent('artifact_edit_result', { result: 'failed' });
expect(fetcher).toHaveBeenCalledTimes(2);
});