Skip to content

Commit

Permalink
[Automatic Import] Add Error handling framework (elastic#193577)
Browse files Browse the repository at this point in the history
## Release Note

Adds error handling framework that provides error message with more
context to user.

## Summary

Relates - [192916](elastic#192916)

This PR adds an error handling framework.
- Add Error classes for specific error scenarios.
- If the error caught is of the predefined Error type the `message` and
`errorCode` is sent back to UI from server.
- The original error message is used to track telemetry and the
errorCode can be translated into a User visible error.
- If there is any non-predefined error server still throws a
`badRequest` with the error message.

This PR also adds/updates the graph images for different langgraphs

## Screenshots for error messages
<img width="690" alt="image"
src="https://github.com/user-attachments/assets/bb848ce7-e474-4e4e-8d07-59b534c543ea">

<img width="691" alt="image"
src="https://github.com/user-attachments/assets/fbf4cf46-9bbe-4c37-aaaa-0ede1cdcba7c">

### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

### For maintainers

- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

(cherry picked from commit 9fb6f55)
  • Loading branch information
bhapas committed Sep 25, 2024
1 parent c7b153c commit d9145da
Show file tree
Hide file tree
Showing 22 changed files with 332 additions and 20 deletions.
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.
Binary file modified x-pack/plugins/integration_assistant/docs/imgs/ecs_subgraph.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.
Binary file modified x-pack/plugins/integration_assistant/docs/imgs/related_graph.png
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
14 changes: 12 additions & 2 deletions x-pack/plugins/integration_assistant/server/routes/ecs_routes.ts
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

0 comments on commit d9145da

Please sign in to comment.