Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion static/app/components/pipeline/index.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ function PipelineRunner({
</Flex>
</Container>

{pipeline.error && <Text variant="muted">Error: {pipeline.error.message}</Text>}
{pipeline.error && <Text variant="muted">Error: {pipeline.error}</Text>}

{pipeline.view}

Expand Down
2 changes: 1 addition & 1 deletion static/app/components/pipeline/modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ function PipelineModal<
<Alert.Button onClick={pipeline.restart}>{t('Start over')}</Alert.Button>
}
>
{pipeline.error.message}
{pipeline.error}
</Alert>
)}
{pipeline.view}
Expand Down
11 changes: 9 additions & 2 deletions static/app/components/pipeline/pipelineIntegrationGitLab.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {useCallback} from 'react';
import {useCallback, useEffect} from 'react';
import {z} from 'zod';

import {CodeBlock} from '@sentry/scraps/code';
import {defaultFormOptions, useScrapsForm} from '@sentry/scraps/form';
import {defaultFormOptions, setFieldErrors, useScrapsForm} from '@sentry/scraps/form';
import {Flex, Stack} from '@sentry/scraps/layout';
import {Text} from '@sentry/scraps/text';

Expand Down Expand Up @@ -50,6 +50,7 @@ interface InstallationConfigAdvanceData {
function InstallationConfigStep({
stepData,
advance,
advanceError,
isAdvancing,
}: PipelineStepProps<InstallationConfigStepData, InstallationConfigAdvanceData>) {
const defaults = stepData.defaults ?? {};
Expand Down Expand Up @@ -79,6 +80,12 @@ function InstallationConfigStep({
},
});

useEffect(() => {
if (advanceError) {
setFieldErrors(form, advanceError);
}
}, [advanceError, form]);
Comment thread
evanpurkhiser marked this conversation as resolved.

const configForm = (
<form.AppForm form={form}>
<Stack gap="lg">
Expand Down
6 changes: 4 additions & 2 deletions static/app/components/pipeline/types.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {RequestError} from 'sentry/utils/requestError/requestError';

const PIPELINE_NAME_MAP = {
integration: 'integration_pipeline',
identity: 'identity_provider',
Expand Down Expand Up @@ -81,7 +83,7 @@ export interface PipelineStepProps<
A = Record<string, unknown>,
> {
advance: (data?: A) => void;
advanceError: Error | null;
advanceError: RequestError | null;
isAdvancing: boolean;
stepData: D;
stepIndex: number;
Expand Down Expand Up @@ -127,7 +129,7 @@ export interface PipelineAdvanceResponse {
export interface ApiPipeline<C = Record<string, unknown>> {
completionData: C | null;
definition: PipelineDefinition;
error: Error | null;
error: string | null;
isAdvancing: boolean;
isComplete: boolean;
isInitializing: boolean;
Expand Down
2 changes: 1 addition & 1 deletion static/app/components/pipeline/usePipeline.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ function TestHarness({onComplete}: {onComplete?: (data: any) => void} = {}) {
<div data-test-id="is-initializing">{String(pipeline.isInitializing)}</div>
<div data-test-id="is-advancing">{String(pipeline.isAdvancing)}</div>
<div data-test-id="is-complete">{String(pipeline.isComplete)}</div>
<div data-test-id="error">{pipeline.error?.message ?? 'none'}</div>
<div data-test-id="error">{pipeline.error ?? 'none'}</div>
<div data-test-id="completion-data">
{pipeline.completionData ? JSON.stringify(pipeline.completionData) : 'none'}
</div>
Expand Down
45 changes: 38 additions & 7 deletions static/app/components/pipeline/usePipeline.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {useCallback, useEffect, useMemo, useRef, useState} from 'react';

import {t} from 'sentry/locale';
import {fetchMutation, useMutation} from 'sentry/utils/queryClient';
import {RequestError} from 'sentry/utils/requestError/requestError';
import {useOrganization} from 'sentry/utils/useOrganization';

import {getPipelineDefinition} from './registry';
Expand Down Expand Up @@ -139,7 +141,7 @@ export function usePipeline<
reset: resetAdvance,
} = useMutation<
PipelineAdvanceResponse,
Error,
RequestError,
Record<string, unknown>,
{generation: number}
>({
Expand Down Expand Up @@ -205,11 +207,24 @@ export function usePipeline<
break;
}
},
onError: (error: Error, _variables, context) => {
onError: (error: RequestError, _variables, context) => {
if (context?.generation !== generationRef.current) {
return;
}
setState({status: 'error', error});
// 404 means the pipeline session expired — unrecoverable.
if (error.status === 404) {
setState({
status: 'error',
error: new Error(t('This flow has expired. Please start over.')),
});
return;
}
// Other 4xx errors are recoverable (e.g. validation failures) and are
// surfaced via advanceError. Only transition to the error state for
// 5xx or unknown errors which are unrecoverable.
if (!error.status || error.status >= 500) {
setState({status: 'error', error});
}
},
});

Expand Down Expand Up @@ -292,10 +307,26 @@ export function usePipeline<
? {stepIndex: state.stepInfo.stepIndex, totalSteps: state.stepInfo.totalSteps}
: {stepIndex: 0, totalSteps: definition.steps.length};

const error =
state.status === 'error'
? state.error
: (advanceError ?? initializeRest.error ?? null);
// Pipeline-level error displayed by the modal as a full-width alert with a
// "Start over" button. When state.status is 'error', the pipeline has hit an
// unrecoverable failure (backend PipelineStepResult.error() response, expired
// session, or 5xx). Otherwise falls back to initialization errors or
// advanceError — but only when it carries a `detail` message (a non-field
// error). Field-level validation errors from advanceError are handled by step
// components via setFieldErrors() and don't surface here.
const advanceDetailError =
advanceError && typeof (advanceError.responseJSON as any)?.detail === 'string'
? ((advanceError.responseJSON as any).detail as string)
: null;

const rawError =
state.status === 'error' ? state.error : (initializeRest.error ?? null);

const error = rawError
? ((rawError instanceof RequestError
? ((rawError.responseJSON as any)?.detail as string | undefined)
: undefined) ?? rawError.message)
: advanceDetailError;
Comment thread
sentry[bot] marked this conversation as resolved.
Comment thread
evanpurkhiser marked this conversation as resolved.

const completionData = state.status === 'complete' ? state.data : null;

Expand Down
Loading