diff --git a/x-pack/plugins/integration_assistant/common/constants.ts b/x-pack/plugins/integration_assistant/common/constants.ts index 296e38c01e71cc..3891c1e5e4343d 100644 --- a/x-pack/plugins/integration_assistant/common/constants.ts +++ b/x-pack/plugins/integration_assistant/common/constants.ts @@ -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', +} diff --git a/x-pack/plugins/integration_assistant/docs/imgs/categorization_graph.png b/x-pack/plugins/integration_assistant/docs/imgs/categorization_graph.png index e7a011ad27b9aa..a15dbf54d905a5 100644 Binary files a/x-pack/plugins/integration_assistant/docs/imgs/categorization_graph.png and b/x-pack/plugins/integration_assistant/docs/imgs/categorization_graph.png differ diff --git a/x-pack/plugins/integration_assistant/docs/imgs/ecs_graph.png b/x-pack/plugins/integration_assistant/docs/imgs/ecs_graph.png index f2342cfc594c3f..49bb9c3243068d 100644 Binary files a/x-pack/plugins/integration_assistant/docs/imgs/ecs_graph.png and b/x-pack/plugins/integration_assistant/docs/imgs/ecs_graph.png differ diff --git a/x-pack/plugins/integration_assistant/docs/imgs/ecs_subgraph.png b/x-pack/plugins/integration_assistant/docs/imgs/ecs_subgraph.png index 85358ee453bcf8..8e3ea2bc5e0ca7 100644 Binary files a/x-pack/plugins/integration_assistant/docs/imgs/ecs_subgraph.png and b/x-pack/plugins/integration_assistant/docs/imgs/ecs_subgraph.png differ diff --git a/x-pack/plugins/integration_assistant/docs/imgs/kv_graph.png b/x-pack/plugins/integration_assistant/docs/imgs/kv_graph.png new file mode 100644 index 00000000000000..48a60999242487 Binary files /dev/null and b/x-pack/plugins/integration_assistant/docs/imgs/kv_graph.png differ diff --git a/x-pack/plugins/integration_assistant/docs/imgs/log_detection_graph.png b/x-pack/plugins/integration_assistant/docs/imgs/log_detection_graph.png index 94830a549dcf11..285e012c57a149 100644 Binary files a/x-pack/plugins/integration_assistant/docs/imgs/log_detection_graph.png and b/x-pack/plugins/integration_assistant/docs/imgs/log_detection_graph.png differ diff --git a/x-pack/plugins/integration_assistant/docs/imgs/related_graph.png b/x-pack/plugins/integration_assistant/docs/imgs/related_graph.png index cebb482a4b495e..73a2c3acac0d4c 100644 Binary files a/x-pack/plugins/integration_assistant/docs/imgs/related_graph.png and b/x-pack/plugins/integration_assistant/docs/imgs/related_graph.png differ diff --git a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/data_stream_step/generation_modal.test.tsx b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/data_stream_step/generation_modal.test.tsx index 3246caa81b0413..a8e6a30ca5dfa4 100644 --- a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/data_stream_step/generation_modal.test.tsx +++ b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/data_stream_step/generation_modal.test.tsx @@ -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( + , + { 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(); diff --git a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/data_stream_step/generation_modal.tsx b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/data_stream_step/generation_modal.tsx index b4a9c05b624502..aefde66ed83dbc 100644 --- a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/data_stream_step/generation_modal.tsx +++ b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/data_stream_step/generation_modal.tsx @@ -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; @@ -171,7 +172,7 @@ 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}` : '' }`; @@ -179,9 +180,14 @@ export const useGeneration = ({ 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); diff --git a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/data_stream_step/translations.ts b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/data_stream_step/translations.ts index d44ad979398581..017a1a9c29caa1 100644 --- a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/data_stream_step/translations.ts +++ b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/data_stream_step/translations.ts @@ -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', @@ -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.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.', + } + ), +}; diff --git a/x-pack/plugins/integration_assistant/server/lib/errors/index.ts b/x-pack/plugins/integration_assistant/server/lib/errors/index.ts new file mode 100644 index 00000000000000..ae3b0093340333 --- /dev/null +++ b/x-pack/plugins/integration_assistant/server/lib/errors/index.ts @@ -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'; diff --git a/x-pack/plugins/integration_assistant/server/lib/errors/recursion_limit_error.ts b/x-pack/plugins/integration_assistant/server/lib/errors/recursion_limit_error.ts new file mode 100644 index 00000000000000..09b307cb841e95 --- /dev/null +++ b/x-pack/plugins/integration_assistant/server/lib/errors/recursion_limit_error.ts @@ -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 } }, + }); + } +} diff --git a/x-pack/plugins/integration_assistant/server/lib/errors/types.ts b/x-pack/plugins/integration_assistant/server/lib/errors/types.ts new file mode 100644 index 00000000000000..2b664da0941e85 --- /dev/null +++ b/x-pack/plugins/integration_assistant/server/lib/errors/types.ts @@ -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; +} diff --git a/x-pack/plugins/integration_assistant/server/lib/errors/unsupported_error.ts b/x-pack/plugins/integration_assistant/server/lib/errors/unsupported_error.ts new file mode 100644 index 00000000000000..79c4f2ccf69a1e --- /dev/null +++ b/x-pack/plugins/integration_assistant/server/lib/errors/unsupported_error.ts @@ -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 } }, + }); + } +} diff --git a/x-pack/plugins/integration_assistant/server/routes/analyze_logs_routes.ts b/x-pack/plugins/integration_assistant/server/routes/analyze_logs_routes.ts index 29a68c4395a7cd..2f0f3db47a7a97 100644 --- a/x-pack/plugins/integration_assistant/server/routes/analyze_logs_routes.ts +++ b/x-pack/plugins/integration_assistant/server/routes/analyze_logs_routes.ts @@ -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 @@ -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 }); } }) ); diff --git a/x-pack/plugins/integration_assistant/server/routes/build_integration_routes.ts b/x-pack/plugins/integration_assistant/server/routes/build_integration_routes.ts index d4b4424f5c8448..1a7ecb58a20622 100644 --- a/x-pack/plugins/integration_assistant/server/routes/build_integration_routes.ts +++ b/x-pack/plugins/integration_assistant/server/routes/build_integration_routes.ts @@ -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 ) { @@ -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 }); } }) ); diff --git a/x-pack/plugins/integration_assistant/server/routes/categorization_routes.ts b/x-pack/plugins/integration_assistant/server/routes/categorization_routes.ts index c6a3cb5d1682a0..635ef08dcdf9c0 100644 --- a/x-pack/plugins/integration_assistant/server/routes/categorization_routes.ts +++ b/x-pack/plugins/integration_assistant/server/routes/categorization_routes.ts @@ -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 @@ -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 }); } } ) diff --git a/x-pack/plugins/integration_assistant/server/routes/ecs_routes.ts b/x-pack/plugins/integration_assistant/server/routes/ecs_routes.ts index 34a9fa5106654c..12d77c66a11322 100644 --- a/x-pack/plugins/integration_assistant/server/routes/ecs_routes.ts +++ b/x-pack/plugins/integration_assistant/server/routes/ecs_routes.ts @@ -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) { router.versioned @@ -92,8 +95,15 @@ export function registerEcsRoutes(router: IRouter) { router.versioned @@ -46,8 +49,15 @@ export function registerPipelineRoutes(router: IRouter) { router.versioned @@ -89,8 +92,15 @@ export function registerRelatedRoutes(router: IRouter { + it('should throw a RecursionLimitError when given a GraphRecursionError', () => { + const errorMessage = 'Recursion limit exceeded'; + const errorCode = ErrorCode.RECURSION_LIMIT; + const recursionError = new GraphRecursionError(errorMessage); + + expect(() => { + handleCustomErrors(recursionError, errorCode); + }).toThrow(RecursionLimitError); + expect(() => { + handleCustomErrors(recursionError, errorCode); + }).toThrowError(errorMessage); + }); + + it('should rethrow the error when given an error that is not a GraphRecursionError', () => { + const errorMessage = 'Some other error'; + const errorCode = ErrorCode.RECURSION_LIMIT; + const otherError = new Error(errorMessage); + + expect(() => { + handleCustomErrors(otherError, errorCode); + }).toThrow(otherError); + expect(() => { + handleCustomErrors(otherError, errorCode); + }).toThrowError(errorMessage); + }); +}); diff --git a/x-pack/plugins/integration_assistant/server/routes/routes_util.ts b/x-pack/plugins/integration_assistant/server/routes/routes_util.ts new file mode 100644 index 00000000000000..5622392cd06a92 --- /dev/null +++ b/x-pack/plugins/integration_assistant/server/routes/routes_util.ts @@ -0,0 +1,30 @@ +/* + * 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 { GraphRecursionError } from '@langchain/langgraph'; +import { ErrorCode } from '../../common/constants'; +import { RecursionLimitError } from '../lib/errors'; + +/** + * Handles errors that occur during the execution of a function. + * If the error is an instance of GraphRecursionError, it throws a RecursionLimitError with the same message and error code. + * Otherwise, it rethrows the original error. + * + * @param err - The error that occurred. + * @param errorCode - The error code associated with the error. + * @throws {RecursionLimitError} If the error is an instance of GraphRecursionError. + * @throws {Error} The original error. + */ +export function handleCustomErrors( + err: Error, + recursionErrorCode: ErrorCode.RECURSION_LIMIT | ErrorCode.RECURSION_LIMIT_ANALYZE_LOGS +) { + if (err instanceof GraphRecursionError) { + throw new RecursionLimitError(err.message, recursionErrorCode); + } + throw err; +}