Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Automatic Import] Add Error handling framework #193577

Merged
merged 16 commits into from
Sep 25, 2024
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
8 changes: 8 additions & 0 deletions x-pack/plugins/integration_assistant/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,11 @@ export const FLEET_PACKAGES_PATH = `/api/fleet/epm/packages`;

// License
export const MINIMUM_LICENSE_TYPE: LicenseType = 'enterprise';

// ErrorCodes

export enum ErrorCode {
RECURSION_LIMIT = 'recursion-limit',
RECURSION_LIMIT_ANALYZE_LOGS = 'recursion-limit-analyze-logs',
UNSUPPORTED_LOG_SAMPLES_FORMAT = 'unsupported-log-samples-format',
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified x-pack/plugins/integration_assistant/docs/imgs/ecs_graph.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,78 @@ describe('GenerationModal', () => {
);
});

describe('when the retrying successfully', () => {
describe('when retrying successfully', () => {
beforeEach(async () => {
await act(async () => {
result.getByTestId('retryButton').click();
await waitFor(() => expect(mockOnComplete).toBeCalled());
});
});

it('should not render the error callout', () => {
expect(result.queryByTestId('generationErrorCallout')).not.toBeInTheDocument();
});
it('should not render the retry button', () => {
expect(result.queryByTestId('retryButton')).not.toBeInTheDocument();
});
});
});

describe('when there are errors and a message body with error code', () => {
const errorMessage = 'error message';
const errorCode = 'error code';
const error = JSON.stringify({
body: {
message: errorMessage,
attributes: {
errorCode,
},
},
});
let result: RenderResult;
beforeEach(async () => {
mockRunEcsGraph.mockImplementationOnce(() => {
throw new Error(error);
});

await act(async () => {
result = render(
<GenerationModal
integrationSettings={integrationSettings}
connector={connector}
onComplete={mockOnComplete}
onClose={mockOnClose}
/>,
{ wrapper }
);
await waitFor(() =>
expect(result.queryByTestId('generationErrorCallout')).toBeInTheDocument()
);
});
});

it('should show the error text', () => {
expect(result.queryByText(error)).toBeInTheDocument();
});
it('should render the retry button', () => {
expect(result.queryByTestId('retryButton')).toBeInTheDocument();
});
it('should report telemetry for generation error', () => {
expect(mockReportEvent).toHaveBeenCalledWith(
TelemetryEventType.IntegrationAssistantGenerationComplete,
{
sessionId: expect.any(String),
sampleRows: integrationSettings.logSamples?.length ?? 0,
actionTypeId: connector.actionTypeId,
model: expect.anything(),
provider: connector.apiProvider ?? 'unknown',
durationMs: expect.any(Number),
errorMessage: error,
}
);
});

describe('when retrying successfully', () => {
beforeEach(async () => {
await act(async () => {
result.getByTestId('retryButton').click();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { useKibana } from '../../../../../common/hooks/use_kibana';
import type { State } from '../../state';
import * as i18n from './translations';
import { useTelemetry } from '../../../telemetry';
import type { ErrorCode } from '../../../../../../common/constants';

export type OnComplete = (result: State['result']) => void;

Expand Down Expand Up @@ -171,17 +172,22 @@ export const useGeneration = ({
onComplete(result);
} catch (e) {
if (abortController.signal.aborted) return;
const errorMessage = `${e.message}${
const originalErrorMessage = `${e.message}${
e.body ? ` (${e.body.statusCode}): ${e.body.message}` : ''
}`;

reportGenerationComplete({
connector,
integrationSettings,
durationMs: Date.now() - generationStartedAt,
error: errorMessage,
error: originalErrorMessage,
});

let errorMessage = originalErrorMessage;
const errorCode = e.body?.attributes?.errorCode as ErrorCode | undefined;
if (errorCode != null) {
errorMessage = i18n.ERROR_TRANSLATION[errorCode];
}
setError(errorMessage);
} finally {
setIsRequesting(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

import { i18n } from '@kbn/i18n';
import { ErrorCode } from '../../../../../../common/constants';

export const INTEGRATION_NAME_TITLE = i18n.translate(
'xpack.integrationAssistant.step.dataStream.integrationNameTitle',
Expand Down Expand Up @@ -196,3 +197,25 @@ export const GENERATION_ERROR = (progressStep: string) =>
export const RETRY = i18n.translate('xpack.integrationAssistant.step.dataStream.retryButtonLabel', {
defaultMessage: 'Retry',
});

export const ERROR_TRANSLATION: Record<ErrorCode, string> = {
[ErrorCode.RECURSION_LIMIT_ANALYZE_LOGS]: i18n.translate(
'xpack.integrationAssistant.errors.recursionLimitAnalyzeLogsErrorMessage',
{
defaultMessage:
'Please verify the format of log samples is correct and try again. Try with a fewer samples if error persists.',
}
),
[ErrorCode.RECURSION_LIMIT]: i18n.translate(
'xpack.integrationAssistant.errors.recursionLimitReached',
{
defaultMessage: 'Max attempts exceeded. Please try again.',
}
),
[ErrorCode.UNSUPPORTED_LOG_SAMPLES_FORMAT]: i18n.translate(
'xpack.integrationAssistant.errors.unsupportedLogSamples',
{
defaultMessage: 'Unsupported log format in the samples.',
}
),
};
17 changes: 17 additions & 0 deletions x-pack/plugins/integration_assistant/server/lib/errors/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { ErrorThatHandlesItsOwnResponse } from './types';

export function isErrorThatHandlesItsOwnResponse(
e: ErrorThatHandlesItsOwnResponse
): e is ErrorThatHandlesItsOwnResponse {
return typeof (e as ErrorThatHandlesItsOwnResponse).sendResponse === 'function';
}

export { RecursionLimitError } from './recursion_limit_error';
export { UnsupportedLogFormatError } from './unsupported_error';
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { KibanaResponseFactory } from '@kbn/core/server';
import { ErrorThatHandlesItsOwnResponse } from './types';

export class RecursionLimitError extends Error implements ErrorThatHandlesItsOwnResponse {
private readonly errorCode: string;

constructor(message: string, errorCode: string) {
super(message);
this.errorCode = errorCode;
}

public sendResponse(res: KibanaResponseFactory) {
return res.badRequest({
body: { message: this.message, attributes: { errorCode: this.errorCode } },
});
}
}
12 changes: 12 additions & 0 deletions x-pack/plugins/integration_assistant/server/lib/errors/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { KibanaResponseFactory, IKibanaResponse } from '@kbn/core/server';

export interface ErrorThatHandlesItsOwnResponse extends Error {
sendResponse(res: KibanaResponseFactory): IKibanaResponse;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { KibanaResponseFactory } from '@kbn/core/server';
import { ErrorThatHandlesItsOwnResponse } from './types';
import { ErrorCode } from '../../../common/constants';

export class UnsupportedLogFormatError extends Error implements ErrorThatHandlesItsOwnResponse {
private readonly errorCode: string = ErrorCode.UNSUPPORTED_LOG_SAMPLES_FORMAT;

// eslint-disable-next-line @typescript-eslint/no-useless-constructor
constructor(message: string) {
super(message);
}

public sendResponse(res: KibanaResponseFactory) {
return res.customError({
statusCode: 501,
body: { message: this.message, attributes: { errorCode: this.errorCode } },
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import type { IntegrationAssistantRouteHandlerContext } from '../plugin';
import { getLLMClass, getLLMType } from '../util/llm';
import { buildRouteValidationWithZod } from '../util/route_validation';
import { withAvailability } from './with_availability';
import { isErrorThatHandlesItsOwnResponse, UnsupportedLogFormatError } from '../lib/errors';
import { handleCustomErrors } from './routes_util';
import { ErrorCode } from '../../common/constants';

export function registerAnalyzeLogsRoutes(
router: IRouter<IntegrationAssistantRouteHandlerContext>
Expand Down Expand Up @@ -82,14 +85,18 @@ export function registerAnalyzeLogsRoutes(
const graphResults = await graph.invoke(logFormatParameters, options);
const graphLogFormat = graphResults.results.samplesFormat.name;
if (graphLogFormat === 'unsupported' || graphLogFormat === 'csv') {
return res.customError({
statusCode: 501,
body: { message: `Unsupported log samples format` },
});
throw new UnsupportedLogFormatError(ErrorCode.UNSUPPORTED_LOG_SAMPLES_FORMAT);
}
return res.ok({ body: AnalyzeLogsResponse.parse(graphResults) });
} catch (e) {
return res.badRequest({ body: e });
} catch (err) {
try {
handleCustomErrors(err, ErrorCode.RECURSION_LIMIT_ANALYZE_LOGS);
} catch (e) {
if (isErrorThatHandlesItsOwnResponse(e)) {
return e.sendResponse(res);
}
}
return res.badRequest({ body: err });
}
})
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import { buildPackage } from '../integration_builder';
import type { IntegrationAssistantRouteHandlerContext } from '../plugin';
import { buildRouteValidationWithZod } from '../util/route_validation';
import { withAvailability } from './with_availability';

import { isErrorThatHandlesItsOwnResponse } from '../lib/errors';
import { handleCustomErrors } from './routes_util';
import { ErrorCode } from '../../common/constants';
export function registerIntegrationBuilderRoutes(
router: IRouter<IntegrationAssistantRouteHandlerContext>
) {
Expand All @@ -38,8 +40,15 @@ export function registerIntegrationBuilderRoutes(
body: zippedIntegration,
headers: { 'Content-Type': 'application/zip' },
});
} catch (e) {
return response.customError({ statusCode: 500, body: e });
} catch (err) {
try {
handleCustomErrors(err, ErrorCode.RECURSION_LIMIT);
} catch (e) {
if (isErrorThatHandlesItsOwnResponse(e)) {
return e.sendResponse(response);
}
}
return response.customError({ statusCode: 500, body: err });
}
})
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ import type { IntegrationAssistantRouteHandlerContext } from '../plugin';
import { getLLMClass, getLLMType } from '../util/llm';
import { buildRouteValidationWithZod } from '../util/route_validation';
import { withAvailability } from './with_availability';
import { isErrorThatHandlesItsOwnResponse } from '../lib/errors';
import { handleCustomErrors } from './routes_util';
import { ErrorCode } from '../../common/constants';

export function registerCategorizationRoutes(
router: IRouter<IntegrationAssistantRouteHandlerContext>
Expand Down Expand Up @@ -98,8 +101,15 @@ export function registerCategorizationRoutes(
const results = await graph.invoke(parameters, options);

return res.ok({ body: CategorizationResponse.parse(results) });
} catch (e) {
return res.badRequest({ body: e });
} catch (err) {
try {
handleCustomErrors(err, ErrorCode.RECURSION_LIMIT);
} catch (e) {
if (isErrorThatHandlesItsOwnResponse(e)) {
return e.sendResponse(res);
}
}
return res.badRequest({ body: err });
}
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import type { IntegrationAssistantRouteHandlerContext } from '../plugin';
import { getLLMClass, getLLMType } from '../util/llm';
import { buildRouteValidationWithZod } from '../util/route_validation';
import { withAvailability } from './with_availability';
import { isErrorThatHandlesItsOwnResponse } from '../lib/errors';
import { handleCustomErrors } from './routes_util';
import { ErrorCode } from '../../common/constants';

export function registerEcsRoutes(router: IRouter<IntegrationAssistantRouteHandlerContext>) {
router.versioned
Expand Down Expand Up @@ -92,8 +95,15 @@ export function registerEcsRoutes(router: IRouter<IntegrationAssistantRouteHandl
const results = await graph.invoke(parameters, options);

return res.ok({ body: EcsMappingResponse.parse(results) });
} catch (e) {
return res.badRequest({ body: e });
} catch (err) {
try {
handleCustomErrors(err, ErrorCode.RECURSION_LIMIT);
} catch (e) {
if (isErrorThatHandlesItsOwnResponse(e)) {
return e.sendResponse(res);
}
}
return res.badRequest({ body: err });
}
})
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import type { IntegrationAssistantRouteHandlerContext } from '../plugin';
import { testPipeline } from '../util/pipeline';
import { buildRouteValidationWithZod } from '../util/route_validation';
import { withAvailability } from './with_availability';
import { isErrorThatHandlesItsOwnResponse } from '../lib/errors';
import { handleCustomErrors } from './routes_util';
import { ErrorCode } from '../../common/constants';

export function registerPipelineRoutes(router: IRouter<IntegrationAssistantRouteHandlerContext>) {
router.versioned
Expand Down Expand Up @@ -46,8 +49,15 @@ export function registerPipelineRoutes(router: IRouter<IntegrationAssistantRoute
return res.ok({
body: CheckPipelineResponse.parse({ results: { docs: pipelineResults } }),
});
} catch (e) {
return res.badRequest({ body: e });
} catch (err) {
try {
handleCustomErrors(err, ErrorCode.RECURSION_LIMIT);
} catch (e) {
if (isErrorThatHandlesItsOwnResponse(e)) {
return e.sendResponse(res);
}
}
return res.badRequest({ body: err });
}
}
)
Expand Down
Loading