mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
Co-authored-by: sanil-23 <sanil@alphahuman.xyz> Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
co-authored by
sanil-23
Claude
parent
997be4eef2
commit
cc498d1727
@@ -1,3 +1,4 @@
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { trackEvent } from '../../../services/analytics';
|
||||
@@ -15,8 +16,20 @@ const ContextPage = () => {
|
||||
// the final step when it runs — finish onboarding directly.
|
||||
onNext={() => {
|
||||
trackEvent('onboarding_step_complete', { step_name: 'context' });
|
||||
// The completeAndExit chain awaits four core RPCs
|
||||
// (app_state_update_local_state → app_state_snapshot →
|
||||
// config_set_onboarding_completed → app_state_snapshot). When any
|
||||
// rejects, the user sees a silent dead "Continue to chat" button
|
||||
// (#2081). The `.catch` below kept the rejection out of Sentry's
|
||||
// global handlers because a handled rejection never fires
|
||||
// `unhandledrejection`. Forward to Sentry explicitly so the
|
||||
// dashboard can show whether #2179 (snapshot timeout) closed the
|
||||
// symptom in practice.
|
||||
void completeAndExit().catch(error => {
|
||||
console.error('[onboarding:context-page] completeAndExit failed', error);
|
||||
Sentry.captureException(error, {
|
||||
tags: { flow: 'onboarding-complete', step: 'continue-to-chat' },
|
||||
});
|
||||
});
|
||||
}}
|
||||
onBack={() => navigate('/onboarding/skills')}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Verifies that `ContextPage` forwards a `completeAndExit` rejection into
|
||||
* Sentry with the documented tags (#2081). The handled `.catch` would
|
||||
* otherwise keep the failure out of `Sentry.globalHandlersIntegration`, so
|
||||
* an explicit capture is the only way the dashboard sees it.
|
||||
*/
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { renderWithProviders } from '../../../../test/test-utils';
|
||||
import { OnboardingContext, type OnboardingContextValue } from '../../OnboardingContext';
|
||||
import ContextPage from '../ContextPage';
|
||||
|
||||
const hoisted = vi.hoisted(() => ({ captureException: vi.fn() }));
|
||||
|
||||
vi.mock('@sentry/react', () => ({ captureException: hoisted.captureException }));
|
||||
|
||||
// `trackEvent` writes through to analytics → react-ga4 / Sentry. Stub it so
|
||||
// the rejection-capture assertion isn't entangled with consent / GA wiring.
|
||||
vi.mock('../../../../services/analytics', () => ({ trackEvent: vi.fn() }));
|
||||
|
||||
// The page renders `ContextGatheringStep`, which auto-starts a heavy pipeline
|
||||
// on mount when `connectedSources` includes a Composio Gmail entry. Stub it
|
||||
// to a button that simply invokes `onNext` so the failure path is reachable
|
||||
// without simulating the full pipeline.
|
||||
vi.mock('../../steps/ContextGatheringStep', () => ({
|
||||
default: ({ onNext }: { onNext: () => void | Promise<void> }) => (
|
||||
<button data-testid="continue-to-chat" onClick={() => onNext()}>
|
||||
Continue to chat
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
function renderContextPage(completeAndExit: OnboardingContextValue['completeAndExit']) {
|
||||
const value: OnboardingContextValue = {
|
||||
draft: { connectedSources: ['composio:gmail'] },
|
||||
setDraft: vi.fn(),
|
||||
completeAndExit,
|
||||
};
|
||||
return renderWithProviders(
|
||||
<OnboardingContext.Provider value={value}>
|
||||
<ContextPage />
|
||||
</OnboardingContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
describe('ContextPage — onboarding-complete Sentry capture (#2081)', () => {
|
||||
beforeEach(() => {
|
||||
hoisted.captureException.mockReset();
|
||||
});
|
||||
|
||||
it('captures a completeAndExit rejection to Sentry with the documented tags', async () => {
|
||||
const failure = new Error('app_state_snapshot timed out');
|
||||
const completeAndExit = vi.fn().mockRejectedValue(failure);
|
||||
|
||||
renderContextPage(completeAndExit);
|
||||
fireEvent.click(screen.getByTestId('continue-to-chat'));
|
||||
|
||||
await waitFor(() => expect(hoisted.captureException).toHaveBeenCalledTimes(1));
|
||||
expect(completeAndExit).toHaveBeenCalledTimes(1);
|
||||
|
||||
const [thrown, ctx] = hoisted.captureException.mock.calls[0];
|
||||
expect(thrown).toBe(failure);
|
||||
expect(ctx).toEqual({ tags: { flow: 'onboarding-complete', step: 'continue-to-chat' } });
|
||||
});
|
||||
|
||||
it('does not capture anything when completeAndExit resolves', async () => {
|
||||
const completeAndExit = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
renderContextPage(completeAndExit);
|
||||
fireEvent.click(screen.getByTestId('continue-to-chat'));
|
||||
|
||||
// Let the resolved promise settle.
|
||||
await waitFor(() => expect(completeAndExit).toHaveBeenCalledTimes(1));
|
||||
expect(hoisted.captureException).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -12,6 +12,7 @@
|
||||
* External calls still go through core (auth, proxy, billing). Only the
|
||||
* stage-by-stage orchestration lives in the renderer.
|
||||
*/
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { useT } from '../../../lib/i18n/I18nContext';
|
||||
@@ -372,6 +373,13 @@ const ContextGatheringStep = ({
|
||||
const t = setTimeout(() => {
|
||||
void Promise.resolve(onNext()).catch(e => {
|
||||
console.warn('[onboarding:context] auto-advance failed', e);
|
||||
// Mirrors the manual click capture in ContextPage so the auto-
|
||||
// advance failure mode is not a Sentry blind spot (#2081). The
|
||||
// step tag distinguishes it from `continue-to-chat` clicks in
|
||||
// the dashboard.
|
||||
Sentry.captureException(e, {
|
||||
tags: { flow: 'onboarding-complete', step: 'auto-advance' },
|
||||
});
|
||||
setHasError(true);
|
||||
});
|
||||
}, 800);
|
||||
|
||||
@@ -9,11 +9,13 @@ const getCoreRpcUrl = vi.hoisted(() => vi.fn(async () => 'http://127.0.0.1:7788/
|
||||
const testCoreRpcConnection = vi.hoisted(() =>
|
||||
vi.fn(async () => ({ ok: true, status: 200 }) as Response)
|
||||
);
|
||||
const captureException = vi.hoisted(() => vi.fn());
|
||||
vi.mock('../../../../services/coreRpcClient', () => ({
|
||||
callCoreRpc,
|
||||
getCoreRpcUrl,
|
||||
testCoreRpcConnection,
|
||||
}));
|
||||
vi.mock('@sentry/react', () => ({ captureException }));
|
||||
|
||||
describe('ContextGatheringStep', () => {
|
||||
beforeEach(() => {
|
||||
@@ -21,6 +23,7 @@ describe('ContextGatheringStep', () => {
|
||||
getCoreRpcUrl.mockClear();
|
||||
testCoreRpcConnection.mockClear();
|
||||
testCoreRpcConnection.mockResolvedValue({ ok: true, status: 200 } as Response);
|
||||
captureException.mockReset();
|
||||
});
|
||||
|
||||
it('no-Gmail branch: auto-navigates without any RPC', async () => {
|
||||
@@ -289,6 +292,27 @@ describe('ContextGatheringStep', () => {
|
||||
// just verify the friendly message is shown
|
||||
});
|
||||
|
||||
it('captures auto-advance onNext rejection to Sentry with the documented tags (#2081)', async () => {
|
||||
vi.useFakeTimers();
|
||||
const failure = new Error('app_state_snapshot timed out');
|
||||
const onNext = vi.fn().mockRejectedValue(failure);
|
||||
|
||||
renderWithProviders(<ContextGatheringStep connectedSources={['notion']} onNext={onNext} />);
|
||||
|
||||
// No-Gmail branch finishes synchronously, then auto-advance fires after 800ms.
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(900);
|
||||
});
|
||||
|
||||
expect(onNext).toHaveBeenCalledTimes(1);
|
||||
await vi.waitFor(() => expect(captureException).toHaveBeenCalledTimes(1));
|
||||
const [thrown, ctx] = captureException.mock.calls[0];
|
||||
expect(thrown).toBe(failure);
|
||||
expect(ctx).toEqual({ tags: { flow: 'onboarding-complete', step: 'auto-advance' } });
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// #2156 — slow-but-alive snapshot / save_profile path
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user