diff --git a/x-pack/plugins/reporting/common/types/index.ts b/x-pack/plugins/reporting/common/types/index.ts
index 42845297e204e..b9ffe09b5fe5c 100644
--- a/x-pack/plugins/reporting/common/types/index.ts
+++ b/x-pack/plugins/reporting/common/types/index.ts
@@ -32,7 +32,6 @@ export interface ReportDocumentHead {
export interface ReportOutput extends TaskRunResult {
content: string | null;
- error_code?: string;
size: number;
}
@@ -63,6 +62,16 @@ export interface TaskRunResult {
max_size_reached?: boolean;
warnings?: string[];
metrics?: TaskRunMetrics;
+
+ /**
+ * When running a report task we may finish with warnings that were triggered
+ * by an error. We can pass the error code via the task run result to the
+ * task runner so that it can be recorded for telemetry.
+ *
+ * Alternatively, this field can be populated in the event that the task does
+ * not complete in the task runner's error handler.
+ */
+ error_code?: string;
}
export interface ReportSource {
diff --git a/x-pack/plugins/reporting/public/lib/stream_handler.ts b/x-pack/plugins/reporting/public/lib/stream_handler.ts
index e9645f3bb8735..80932f8ddeeee 100644
--- a/x-pack/plugins/reporting/public/lib/stream_handler.ts
+++ b/x-pack/plugins/reporting/public/lib/stream_handler.ts
@@ -13,10 +13,11 @@ import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY, JOB_STATUSES } from '../../co
import { JobId, JobSummary, JobSummarySet } from '../../common/types';
import {
getFailureToast,
- getGeneralErrorToast,
+ getWarningToast,
getSuccessToast,
- getWarningFormulasToast,
+ getGeneralErrorToast,
getWarningMaxSizeToast,
+ getWarningFormulasToast,
} from '../notifier';
import { Job } from './job';
import { ReportingAPIClient } from './reporting_api_client';
@@ -71,6 +72,15 @@ export class ReportingNotifierStreamHandler {
this.theme
)
);
+ } else if (job.status === JOB_STATUSES.WARNINGS) {
+ this.notifications.toasts.addWarning(
+ getWarningToast(
+ job,
+ this.apiClient.getManagementLink,
+ this.apiClient.getDownloadLink,
+ this.theme
+ )
+ );
} else {
this.notifications.toasts.addSuccess(
getSuccessToast(
diff --git a/x-pack/plugins/reporting/public/notifier/index.ts b/x-pack/plugins/reporting/public/notifier/index.ts
index b44f1e9169747..d687e7009fb4c 100644
--- a/x-pack/plugins/reporting/public/notifier/index.ts
+++ b/x-pack/plugins/reporting/public/notifier/index.ts
@@ -10,3 +10,4 @@ export { getGeneralErrorToast } from './general_error';
export { getSuccessToast } from './job_success';
export { getWarningFormulasToast } from './job_warning_formulas';
export { getWarningMaxSizeToast } from './job_warning_max_size';
+export { getWarningToast } from './job_warning';
diff --git a/x-pack/plugins/reporting/public/notifier/job_warning.tsx b/x-pack/plugins/reporting/public/notifier/job_warning.tsx
new file mode 100644
index 0000000000000..2ac10216f3f19
--- /dev/null
+++ b/x-pack/plugins/reporting/public/notifier/job_warning.tsx
@@ -0,0 +1,46 @@
+/*
+ * 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 { FormattedMessage } from '@kbn/i18n-react';
+import React, { Fragment } from 'react';
+import { ThemeServiceStart, ToastInput } from 'src/core/public';
+import { toMountPoint } from '../../../../../src/plugins/kibana_react/public';
+import { JobId, JobSummary } from '../../common/types';
+import { DownloadButton } from './job_download_button';
+import { ReportLink } from './report_link';
+
+export const getWarningToast = (
+ job: JobSummary,
+ getReportLink: () => string,
+ getDownloadLink: (jobId: JobId) => string,
+ theme: ThemeServiceStart
+): ToastInput => ({
+ title: toMountPoint(
+ ,
+ { theme$: theme.theme$ }
+ ),
+ text: toMountPoint(
+
+
+
+
+
+
+
+
+ ,
+ { theme$: theme.theme$ }
+ ),
+ 'data-test-subj': 'completeReportWarning',
+});
diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.test.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.test.ts
index eb3cbd6118eb9..c525cb7c0def2 100644
--- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.test.ts
+++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.test.ts
@@ -27,7 +27,7 @@ import {
UI_SETTINGS_CSV_SEPARATOR,
UI_SETTINGS_DATEFORMAT_TZ,
} from '../../../../common/constants';
-import { AuthenticationExpiredError } from '../../../../common/errors';
+import { UnknownError } from '../../../../common/errors';
import {
createMockConfig,
createMockConfigSchema,
@@ -808,25 +808,76 @@ it('can override ignoring frozen indices', async () => {
);
});
-it('throws an AuthenticationExpiredError when ES does not accept credentials', async () => {
- mockDataClient.search = jest.fn().mockImplementation(() => {
- throw new esErrors.ResponseError({ statusCode: 403, meta: {} as any, warnings: [] });
+describe('error codes', () => {
+ it('returns the expected error code when authentication expires', async () => {
+ mockDataClient.search = jest.fn().mockImplementation(() =>
+ Rx.of({
+ rawResponse: {
+ _scroll_id: 'test',
+ hits: {
+ hits: range(0, 5).map(() => ({
+ fields: {
+ date: ['2020-12-31T00:14:28.000Z'],
+ ip: ['110.135.176.89'],
+ message: ['super cali fragile istic XPLA docious'],
+ },
+ })),
+ total: 10,
+ },
+ },
+ })
+ );
+
+ mockEsClient.asCurrentUser.scroll = jest.fn().mockImplementation(() => {
+ throw new esErrors.ResponseError({ statusCode: 403, meta: {} as any, warnings: [] });
+ });
+
+ const generateCsv = new CsvGenerator(
+ createMockJob({ columns: ['date', 'ip', 'message'] }),
+ mockConfig,
+ {
+ es: mockEsClient,
+ data: mockDataClient,
+ uiSettings: uiSettingsClient,
+ },
+ {
+ searchSourceStart: mockSearchSourceService,
+ fieldFormatsRegistry: mockFieldFormatsRegistry,
+ },
+ new CancellationToken(),
+ logger,
+ stream
+ );
+
+ const { error_code: errorCode, warnings } = await generateCsv.generateData();
+ expect(errorCode).toBe('authentication_expired');
+ expect(warnings).toMatchInlineSnapshot(`
+ Array [
+ "This report contains partial CSV results because authentication expired before it could finish. Try exporting a smaller amount of data or increase your authentication timeout.",
+ ]
+ `);
+ });
+
+ it('throws for unknown errors', async () => {
+ mockDataClient.search = jest.fn().mockImplementation(() => {
+ throw new esErrors.ResponseError({ statusCode: 500, meta: {} as any, warnings: [] });
+ });
+ const generateCsv = new CsvGenerator(
+ createMockJob({ columns: ['date', 'ip', 'message'] }),
+ mockConfig,
+ {
+ es: mockEsClient,
+ data: mockDataClient,
+ uiSettings: uiSettingsClient,
+ },
+ {
+ searchSourceStart: mockSearchSourceService,
+ fieldFormatsRegistry: mockFieldFormatsRegistry,
+ },
+ new CancellationToken(),
+ logger,
+ stream
+ );
+ await expect(generateCsv.generateData()).rejects.toBeInstanceOf(UnknownError);
});
- const generateCsv = new CsvGenerator(
- createMockJob({ columns: ['date', 'ip', 'message'] }),
- mockConfig,
- {
- es: mockEsClient,
- data: mockDataClient,
- uiSettings: uiSettingsClient,
- },
- {
- searchSourceStart: mockSearchSourceService,
- fieldFormatsRegistry: mockFieldFormatsRegistry,
- },
- new CancellationToken(),
- logger,
- stream
- );
- await expect(generateCsv.generateData()).rejects.toEqual(new AuthenticationExpiredError());
});
diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts
index 5be17f5e6d252..201484af9d7d0 100644
--- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts
+++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts
@@ -7,7 +7,6 @@
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { errors as esErrors } from '@elastic/elasticsearch';
-import { i18n } from '@kbn/i18n';
import type { IScopedClusterClient, IUiSettingsClient } from 'src/core/server';
import type { IScopedSearchClient } from 'src/plugins/data/server';
import type { Datatable } from 'src/plugins/expressions/server';
@@ -31,13 +30,18 @@ import type {
import { KbnServerError } from '../../../../../../../src/plugins/kibana_utils/server';
import type { CancellationToken } from '../../../../common/cancellation_token';
import { CONTENT_TYPE_CSV } from '../../../../common/constants';
-import { AuthenticationExpiredError } from '../../../../common/errors';
+import {
+ AuthenticationExpiredError,
+ UnknownError,
+ ReportingError,
+} from '../../../../common/errors';
import { byteSizeValueToNumber } from '../../../../common/schema_utils';
import type { LevelLogger } from '../../../lib';
import type { TaskRunResult } from '../../../lib/tasks';
import type { JobParamsCSV } from '../types';
import { CsvExportSettings, getExportSettings } from './get_export_settings';
import { MaxSizeStringBuilder } from './max_size_string_builder';
+import { i18nTexts } from './i18n_texts';
interface Clients {
es: IScopedClusterClient;
@@ -257,6 +261,7 @@ export class CsvGenerator {
),
this.dependencies.searchSourceStart.create(this.job.searchSource),
]);
+ let reportingError: undefined | ReportingError;
const index = searchSource.getField('index');
@@ -360,19 +365,19 @@ export class CsvGenerator {
// Add warnings to be logged
if (this.csvContainsFormulas && escapeFormulaValues) {
- warnings.push(
- i18n.translate('xpack.reporting.exportTypes.csv.generateCsv.escapedFormulaValues', {
- defaultMessage: 'CSV may contain formulas whose values have been escaped',
- })
- );
+ warnings.push(i18nTexts.escapedFormulaValuesMessage);
}
} catch (err) {
this.logger.error(err);
if (err instanceof KbnServerError && err.errBody) {
throw JSON.stringify(err.errBody.error);
}
+
if (err instanceof esErrors.ResponseError && [401, 403].includes(err.statusCode ?? 0)) {
- throw new AuthenticationExpiredError();
+ reportingError = new AuthenticationExpiredError();
+ warnings.push(i18nTexts.authenticationError.partialResultsMessage);
+ } else {
+ throw new UnknownError(err.message);
}
} finally {
// clear scrollID
@@ -405,6 +410,7 @@ export class CsvGenerator {
csv: { rows: this.csvRowCount },
},
warnings,
+ error_code: reportingError?.code,
};
}
}
diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/i18n_texts.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/i18n_texts.ts
new file mode 100644
index 0000000000000..c994226c6a05c
--- /dev/null
+++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/i18n_texts.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 { i18n } from '@kbn/i18n';
+
+export const i18nTexts = {
+ escapedFormulaValuesMessage: i18n.translate(
+ 'xpack.reporting.exportTypes.csv.generateCsv.escapedFormulaValues',
+ {
+ defaultMessage: 'CSV may contain formulas whose values have been escaped',
+ }
+ ),
+ authenticationError: {
+ partialResultsMessage: i18n.translate(
+ 'xpack.reporting.exportTypes.csv.generateCsv.authenticationExpired.partialResultsMessage',
+ {
+ defaultMessage:
+ 'This report contains partial CSV results because authentication expired before it could finish. Try exporting a smaller amount of data or increase your authentication timeout.',
+ }
+ ),
+ },
+};
diff --git a/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts b/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts
index 019f128e5f07d..449f3b8da7671 100644
--- a/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts
+++ b/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts
@@ -227,6 +227,7 @@ export class ExecuteReportTask implements ReportingTask {
docOutput.size = output.size;
docOutput.warnings =
output.warnings && output.warnings.length > 0 ? output.warnings : undefined;
+ docOutput.error_code = output.error_code;
} else {
const defaultOutput = null;
docOutput.content = output.toString() || defaultOutput;