diff --git a/.buildkite/scripts/pipelines/pull_request/pipeline.ts b/.buildkite/scripts/pipelines/pull_request/pipeline.ts index cadfa23b53f9d..6fe1bb1b251e1 100644 --- a/.buildkite/scripts/pipelines/pull_request/pipeline.ts +++ b/.buildkite/scripts/pipelines/pull_request/pipeline.ts @@ -119,6 +119,15 @@ const uploadPipeline = (pipelineContent: string | object) => { pipeline.push(getPipeline('.buildkite/pipelines/pull_request/apm_cypress.yml')); } + if ( + (await doAnyChangesMatch([/^x-pack\/plugins\/observability_onboarding/])) || + GITHUB_PR_LABELS.includes('ci:all-cypress-suites') + ) { + pipeline.push( + getPipeline('.buildkite/pipelines/pull_request/observability_onboarding_cypress.yml') + ); + } + if ( (await doAnyChangesMatch([/^x-pack\/plugins\/profiling/])) || GITHUB_PR_LABELS.includes('ci:all-cypress-suites') diff --git a/docs/apm/api.asciidoc b/docs/apm/api.asciidoc index 9058b29f0cb8a..fb672b2884af2 100644 --- a/docs/apm/api.asciidoc +++ b/docs/apm/api.asciidoc @@ -11,7 +11,6 @@ Some APM app features are provided via a REST API: * <> * <> * <> -* <> * <> [float] @@ -716,219 +715,6 @@ curl -X DELETE "http://localhost:5601/api/apm/sourcemaps/apm:foo-1.0.0-644fd5a9" ******************************************************* //// -[role="xpack"] -[[android-sourcemap-api]] -=== Android source map API - -IMPORTANT: This endpoint is only compatible with the -{apm-guide-ref}/index.html[APM integration for Elastic Agent]. - -An Android source map (generated using Android's https://developer.android.com/build/shrink-code[R8 tool]) -allows obfuscated app stacktraces to be mapped back to original source code -- -allowing you to maintain the size and security of minimized code, without losing the ability to debug your application. - -For best results, uploading source maps should become a part of your deployment procedure, -and not something you only do when you see unhelpful errors. -That’s because uploading source maps after errors happen won’t make old errors magically readable -- -errors must occur again for source mapping to occur. - -The following APIs are available: - -* <> -* <> -* <> - -[float] -[[use-android-sourcemap-api]] -==== How to use APM APIs - -.Expand for required headers, privileges, and usage details -[%collapsible%closed] -====== -include::api.asciidoc[tag=using-the-APIs] -====== - -//// -******************************************************* -//// - -[[android-sourcemap-post]] -==== Create or update an Android source map - -Create or update an Android source map for a specific app and version. - -[[android-sourcemap-post-privs]] -===== Privileges - -The user accessing this endpoint requires `All` Kibana privileges for the {beat_kib_app} feature. -For more information, see <>. - -[[android-sourcemap-post-req]] -===== Request - -`POST /api/apm/androidmaps` - -[role="child_attributes"] -[[android-sourcemap-post-req-body]] -===== Request body - -`service_name`:: -(required, string) The name of the Android app that the map should apply to. - -`service_version`:: -(required, string) The version of the Android app that the map should apply to. - -`map_file`:: -(required, string or file upload) The R8-generated map. - -[[android-sourcemap-post-example]] -===== Examples - -The following example uploads a source map for a app named `foo` and a service version of `1.0.0`: - -[source,curl] --------------------------------------------------- -curl -X POST "http://localhost:5601/api/apm/androidmaps" \ --H 'Content-Type: multipart/form-data' \ --H 'kbn-xsrf: true' \ --H 'Authorization: ApiKey ${YOUR_API_KEY}' \ --F 'service_name="foo"' \ --F 'service_version="1.0.0"' \ --F 'map_file=@"/Path/to/the/file/mapping.txt"' --------------------------------------------------- - -[[android-sourcemap-post-body]] -===== Response body - -[source,js] --------------------------------------------------- -{ - "type": "sourcemap", - "identifier": "foo-1.0.0-android", - "relative_url": "/api/fleet/artifacts/foo-1.0.0-android/644fd5a997d1ddd90ee131ba18e2b3d03931d89dd1fe4599143c0b3264b3e456", - "body": "eJyFkL1OwzAUhd/Fc+MbYMuCEBIbHRjKgBgc96R16tiWr1OQqr47NwqJxEK3q/PzWccXxchnZ7E1A1SjuhjVZtF2yOxiEPlO17oWox3D3uPFeSRTjmJQARfCPeiAgGx8NTKsYdAc1T3rwaSJGcds8Sp3c1HnhfywUZ3QhMTFFGepZxqMC9oex3CS9tpk1XyozgOlmoVKuJX1DqEQZ0su7PGtLU+V/3JPKc3cL7TJ2FNDRPov4bFta3MDM4f7W69lpJjLO9qdK8bzVPhcJz3HUCQ4LbO/p5hCSC4cZPByrp/wFqOklbpefwAhzpqI", - "created": "2021-07-09T20:47:44.812Z", - "id": "apm:foo-1.0.0-android-644fd5a997d1ddd90ee131ba18e2b3d03931d89dd1fe4599143c0b3264b3e456", - "compressionAlgorithm": "zlib", - "decodedSha256": "644fd5a997d1ddd90ee131ba18e2b3d03931d89dd1fe4599143c0b3264b3e456", - "decodedSize": 441, - "encodedSha256": "024c72749c3e3dd411b103f7040ae62633558608f480bce4b108cf5b2275bd24", - "encodedSize": 237, - "encryptionAlgorithm": "none", - "packageName": "apm" -} --------------------------------------------------- - -//// -******************************************************* -//// - -[[android-sourcemap-get]] -==== Get source maps - -Returns an array of Fleet artifacts, including source map uploads. - -[[android-sourcemap-get-privs]] -===== Privileges - -The user accessing this endpoint requires `Read` or `All` Kibana privileges for the {beat_kib_app} feature. -For more information, see <>. - -[[android-sourcemap-get-req]] -===== Request - -`GET /api/apm/sourcemaps` - -[[android-sourcemap-get-example]] -===== Example - -The following example requests all uploaded source maps: - -[source,curl] --------------------------------------------------- -curl -X GET "http://localhost:5601/api/apm/sourcemaps" \ --H 'Content-Type: application/json' \ --H 'kbn-xsrf: true' \ --H 'Authorization: ApiKey ${YOUR_API_KEY}' --------------------------------------------------- - -[[android-sourcemap-get-body]] -===== Response body - -[source,js] --------------------------------------------------- -{ - "artifacts": [ - { - "type": "sourcemap", - "identifier": "foo-1.0.0-android", - "relative_url": "/api/fleet/artifacts/foo-1.0.0-android/644fd5a997d1ddd90ee131ba18e2b3d03931d89dd1fe4599143c0b3264b3e456", - "body": { - "serviceName": "foo", - "serviceVersion": "1.0.0", - "bundleFilepath": "android", - "sourceMap": "# compiler: R8\n# compiler_version: 3.2.47\n# min_api: 26\n..." - }, - "created": "2021-07-09T20:47:44.812Z", - "id": "apm:foo-1.0.0-android-644fd5a997d1ddd90ee131ba18e2b3d03931d89dd1fe4599143c0b3264b3e456", - "compressionAlgorithm": "zlib", - "decodedSha256": "644fd5a997d1ddd90ee131ba18e2b3d03931d89dd1fe4599143c0b3264b3e456", - "decodedSize": 441, - "encodedSha256": "024c72749c3e3dd411b103f7040ae62633558608f480bce4b108cf5b2275bd24", - "encodedSize": 237, - "encryptionAlgorithm": "none", - "packageName": "apm" - } - ] -} --------------------------------------------------- - -//// -******************************************************* -//// - -[[android-sourcemap-delete]] -==== Delete source map - -Delete a previously uploaded source map. - -[[android-sourcemap-delete-privs]] -===== Privileges - -The user accessing this endpoint requires `All` Kibana privileges for the {beat_kib_app} feature. -For more information, see <>. - -[[android-sourcemap-delete-req]] -===== Request - -`DELETE /api/apm/sourcemaps/:id` - -[[android-sourcemap-delete-example]] -===== Example - -The following example deletes a source map with an id of `apm:foo-1.0.0-android-644fd5a9`: - -[source,curl] --------------------------------------------------- -curl -X DELETE "http://localhost:5601/api/apm/sourcemaps/apm:foo-1.0.0-android-644fd5a9" \ --H 'Content-Type: application/json' \ --H 'kbn-xsrf: true' \ --H 'Authorization: ApiKey ${YOUR_API_KEY}' --------------------------------------------------- - -[[android-sourcemap-delete-body]] -===== Response body - -[source,js] --------------------------------------------------- -{} --------------------------------------------------- - -//// -******************************************************* -******************************************************* -//// - [role="xpack"] [[agent-key-api]] === APM agent Key API diff --git a/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_error_document/index.ts b/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_error_document/index.ts index c460acf4c3b3f..ffb1158270455 100644 --- a/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_error_document/index.ts +++ b/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_error_document/index.ts @@ -54,14 +54,14 @@ export async function getApmErrorDocument({ }, }); - const error = response.hits.hits[0]?._source as APMError; + const errorDoc = response.hits.hits[0]?._source as APMError; - if (!error) { + if (!errorDoc) { return undefined; } - return pick( - error, + const formattedResponse = pick( + errorDoc, 'message', 'error', '@timestamp', @@ -71,4 +71,11 @@ export async function getApmErrorDocument({ 'span.type', 'span.subtype' ); + + const { error, ...rest } = formattedResponse; + + return { + ...rest, + errorDoc: formattedResponse.error, + }; } diff --git a/x-pack/plugins/apm/server/routes/fleet/source_maps.ts b/x-pack/plugins/apm/server/routes/fleet/source_maps.ts index 4eb0b011cef3c..ad2bc870c7f33 100644 --- a/x-pack/plugins/apm/server/routes/fleet/source_maps.ts +++ b/x-pack/plugins/apm/server/routes/fleet/source_maps.ts @@ -19,18 +19,12 @@ import { getPackagePolicyWithSourceMap } from './get_package_policy_decorators'; const doUnzip = promisify(unzip); -interface ApmMapArtifactBody { +interface ApmSourceMapArtifactBody { serviceName: string; serviceVersion: string; bundleFilepath: string; - sourceMap: string; -} - -interface ApmSourceMapArtifactBody - extends Omit { sourceMap: SourceMap; } - export type ArtifactSourceMap = Omit & { body: ApmSourceMapArtifactBody; }; @@ -110,23 +104,6 @@ export async function createFleetSourceMapArtifact({ }); } -export async function createFleetAndroidMapArtifact({ - apmArtifactBody, - fleetPluginStart, -}: { - apmArtifactBody: ApmMapArtifactBody; - fleetPluginStart: FleetPluginStart; -}) { - const apmArtifactClient = getApmArtifactClient(fleetPluginStart); - const identifier = `${apmArtifactBody.serviceName}-${apmArtifactBody.serviceVersion}-android`; - - return apmArtifactClient.createArtifact({ - type: 'sourcemap', - identifier, - content: JSON.stringify(apmArtifactBody), - }); -} - export async function deleteFleetSourcemapArtifact({ id, fleetPluginStart, diff --git a/x-pack/plugins/apm/server/routes/source_maps/bulk_create_apm_source_maps.ts b/x-pack/plugins/apm/server/routes/source_maps/bulk_create_apm_source_maps.ts index 3aa5c9a780aa3..1495f89f0fceb 100644 --- a/x-pack/plugins/apm/server/routes/source_maps/bulk_create_apm_source_maps.ts +++ b/x-pack/plugins/apm/server/routes/source_maps/bulk_create_apm_source_maps.ts @@ -10,7 +10,7 @@ import { Artifact } from '@kbn/fleet-plugin/server'; import { getUnzippedArtifactBody } from '../fleet/source_maps'; import { APM_SOURCE_MAP_INDEX } from '../settings/apm_indices/apm_system_index_constants'; import { ApmSourceMap } from './create_apm_source_map_index_template'; -import { getEncodedSourceMapContent, getSourceMapId } from './sourcemap_utils'; +import { getEncodedContent, getSourceMapId } from './sourcemap_utils'; export async function bulkCreateApmSourceMaps({ artifacts, @@ -24,7 +24,7 @@ export async function bulkCreateApmSourceMaps({ const { serviceName, serviceVersion, bundleFilepath, sourceMap } = await getUnzippedArtifactBody(artifact.body); - const { contentEncoded, contentHash } = await getEncodedSourceMapContent( + const { contentEncoded, contentHash } = await getEncodedContent( sourceMap ); diff --git a/x-pack/plugins/apm/server/routes/source_maps/create_apm_source_map.ts b/x-pack/plugins/apm/server/routes/source_maps/create_apm_source_map.ts index bda2601080434..95614778024fa 100644 --- a/x-pack/plugins/apm/server/routes/source_maps/create_apm_source_map.ts +++ b/x-pack/plugins/apm/server/routes/source_maps/create_apm_source_map.ts @@ -10,11 +10,7 @@ import { Logger } from '@kbn/core/server'; import { APM_SOURCE_MAP_INDEX } from '../settings/apm_indices/apm_system_index_constants'; import { ApmSourceMap } from './create_apm_source_map_index_template'; import { SourceMap } from './route'; -import { - getEncodedSourceMapContent, - getEncodedContent, - getSourceMapId, -} from './sourcemap_utils'; +import { getEncodedContent, getSourceMapId } from './sourcemap_utils'; export async function createApmSourceMap({ internalESClient, @@ -35,75 +31,9 @@ export async function createApmSourceMap({ serviceName: string; serviceVersion: string; }) { - const { contentEncoded, contentHash } = await getEncodedSourceMapContent( + const { contentEncoded, contentHash } = await getEncodedContent( sourceMapContent ); - return await doCreateApmMap({ - internalESClient, - logger, - fleetId, - created, - bundleFilepath, - serviceName, - serviceVersion, - contentEncoded, - contentHash, - }); -} - -export async function createApmAndroidMap({ - internalESClient, - logger, - fleetId, - created, - mapContent, - bundleFilepath, - serviceName, - serviceVersion, -}: { - internalESClient: ElasticsearchClient; - logger: Logger; - fleetId: string; - created: string; - mapContent: string; - bundleFilepath: string; - serviceName: string; - serviceVersion: string; -}) { - const { contentEncoded, contentHash } = await getEncodedContent(mapContent); - return await doCreateApmMap({ - internalESClient, - logger, - fleetId, - created, - bundleFilepath, - serviceName, - serviceVersion, - contentEncoded, - contentHash, - }); -} -async function doCreateApmMap({ - internalESClient, - logger, - fleetId, - created, - bundleFilepath, - serviceName, - serviceVersion, - contentEncoded, - contentHash, -}: { - internalESClient: ElasticsearchClient; - logger: Logger; - fleetId: string; - created: string; - bundleFilepath: string; - serviceName: string; - serviceVersion: string; - contentEncoded: string; - contentHash: string; -}) { const doc: ApmSourceMap = { fleet_id: fleetId, created, diff --git a/x-pack/plugins/apm/server/routes/source_maps/route.ts b/x-pack/plugins/apm/server/routes/source_maps/route.ts index c0cc2404af492..ae29267644668 100644 --- a/x-pack/plugins/apm/server/routes/source_maps/route.ts +++ b/x-pack/plugins/apm/server/routes/source_maps/route.ts @@ -9,24 +9,19 @@ import { SavedObjectsClientContract } from '@kbn/core/server'; import { Artifact } from '@kbn/fleet-plugin/server'; import { jsonRt, toNumberRt } from '@kbn/io-ts-utils'; import * as t from 'io-ts'; -import { either } from 'fp-ts/lib/Either'; import { ApmFeatureFlags } from '../../../common/apm_feature_flags'; import { getInternalSavedObjectsClient } from '../../lib/helpers/get_internal_saved_objects_client'; import { stringFromBufferRt } from '../../utils/string_from_buffer_rt'; import { createApmServerRoute } from '../apm_routes/create_apm_server_route'; import { createFleetSourceMapArtifact, - createFleetAndroidMapArtifact, deleteFleetSourcemapArtifact, getCleanedBundleFilePath, listSourceMapArtifacts, ListSourceMapArtifactsResponse, updateSourceMapsOnFleetPolicies, } from '../fleet/source_maps'; -import { - createApmSourceMap, - createApmAndroidMap, -} from './create_apm_source_map'; +import { createApmSourceMap } from './create_apm_source_map'; import { deleteApmSourceMap } from './delete_apm_sourcemap'; import { runFleetSourcemapArtifactsMigration } from './schedule_source_map_migration'; @@ -46,24 +41,6 @@ export const sourceMapRt = t.intersection([ export type SourceMap = t.TypeOf; -const androidMapValidation = new t.Type( - 'ANDROID_MAP_VALIDATION', - t.string.is, - (input, context): t.Validation => - either.chain( - t.string.validate(input, context), - (str): t.Validation => { - const firstLine = str.split('\n', 1)[0]; - if (firstLine.trim() === '# compiler: R8') { - return t.success(str); - } else { - return t.failure(input, context); - } - } - ), - (a): string => a -); - function throwNotImplementedIfSourceMapNotAvailable( featureFlags: ApmFeatureFlags ): void { @@ -114,7 +91,7 @@ const uploadSourceMapRoute = createApmServerRoute({ endpoint: 'POST /api/apm/sourcemaps 2023-10-31', options: { tags: ['access:apm', 'access:apm_write'], - body: { accepts: ['multipart/form-data'], maxBytes: 100 * 1024 * 1024 }, + body: { accepts: ['multipart/form-data'] }, }, params: t.type({ body: t.type({ @@ -192,85 +169,6 @@ const uploadSourceMapRoute = createApmServerRoute({ }, }); -const uploadAndroidMapRoute = createApmServerRoute({ - endpoint: 'POST /api/apm/androidmaps 2023-10-31', - options: { - tags: ['access:apm', 'access:apm_write'], - body: { accepts: ['multipart/form-data'], maxBytes: 100 * 1024 * 1024 }, - }, - params: t.type({ - body: t.type({ - service_name: t.string, - service_version: t.string, - map_file: t - .union([t.string, stringFromBufferRt]) - .pipe(androidMapValidation), - }), - }), - handler: async ({ - params, - plugins, - core, - logger, - featureFlags, - }): Promise => { - throwNotImplementedIfSourceMapNotAvailable(featureFlags); - - const { - service_name: serviceName, - service_version: serviceVersion, - map_file: sourceMapContent, - } = params.body; - const bundleFilepath = 'android'; - const fleetPluginStart = await plugins.fleet?.start(); - const coreStart = await core.start(); - const internalESClient = coreStart.elasticsearch.client.asInternalUser; - const savedObjectsClient = await getInternalSavedObjectsClient(coreStart); - try { - if (fleetPluginStart) { - // create source map as fleet artifact - const artifact = await createFleetAndroidMapArtifact({ - fleetPluginStart, - apmArtifactBody: { - serviceName, - serviceVersion, - bundleFilepath, - sourceMap: sourceMapContent, - }, - }); - - // sync source map to APM managed index - await createApmAndroidMap({ - internalESClient, - logger, - fleetId: artifact.id, - created: artifact.created, - mapContent: sourceMapContent, - bundleFilepath, - serviceName, - serviceVersion, - }); - - // sync source map to fleet policy - await updateSourceMapsOnFleetPolicies({ - coreStart, - fleetPluginStart, - savedObjectsClient: - savedObjectsClient as unknown as SavedObjectsClientContract, - internalESClient, - }); - - return artifact; - } - } catch (e) { - throw Boom.internal( - 'Something went wrong while creating a new android map', - e - ); - } - }, -}); - const deleteSourceMapRoute = createApmServerRoute({ endpoint: 'DELETE /api/apm/sourcemaps/{id} 2023-10-31', options: { tags: ['access:apm', 'access:apm_write'] }, @@ -332,6 +230,5 @@ export const sourceMapsRouteRepository = { ...listSourceMapRoute, ...uploadSourceMapRoute, ...deleteSourceMapRoute, - ...uploadAndroidMapRoute, ...migrateFleetArtifactsSourceMapRoute, }; diff --git a/x-pack/plugins/apm/server/routes/source_maps/sourcemap_utils.ts b/x-pack/plugins/apm/server/routes/source_maps/sourcemap_utils.ts index 408d1b98f9c5a..20ff2fa4bd41c 100644 --- a/x-pack/plugins/apm/server/routes/source_maps/sourcemap_utils.ts +++ b/x-pack/plugins/apm/server/routes/source_maps/sourcemap_utils.ts @@ -16,11 +16,8 @@ function asSha256Encoded(content: BinaryLike): string { return createHash('sha256').update(content).digest('hex'); } -export async function getEncodedSourceMapContent(sourceMapContent: SourceMap) { - return getEncodedContent(JSON.stringify(sourceMapContent)); -} -export async function getEncodedContent(textContent: string) { - const contentBuffer = Buffer.from(textContent); +export async function getEncodedContent(sourceMapContent: SourceMap) { + const contentBuffer = Buffer.from(JSON.stringify(sourceMapContent)); const contentZipped = await deflateAsync(contentBuffer); const contentEncoded = contentZipped.toString('base64'); const contentHash = asSha256Encoded(contentZipped); diff --git a/x-pack/plugins/observability_onboarding/e2e/cypress/e2e/logs/feedback.cy.ts b/x-pack/plugins/observability_onboarding/e2e/cypress/e2e/logs/feedback.cy.ts new file mode 100644 index 0000000000000..495fd5672bfb9 --- /dev/null +++ b/x-pack/plugins/observability_onboarding/e2e/cypress/e2e/logs/feedback.cy.ts @@ -0,0 +1,29 @@ +/* + * 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. + */ + +describe('[Logs onboarding] Give Feedback', () => { + beforeEach(() => { + cy.loginAsElastic(); + cy.visitKibana('/app/observabilityOnboarding'); + }); + + it('feedback button is present in system logs onboarding', () => { + cy.getByTestSubj('obltOnboardingHomeStartSystemLogStream').click(); + cy.getByTestSubj('observabilityOnboardingPageGiveFeedback').should('exist'); + }); + + it('feedback button is present in custom logs onboarding', () => { + cy.getByTestSubj('obltOnboardingHomeStartLogFileStream').click(); + cy.getByTestSubj('observabilityOnboardingPageGiveFeedback').should('exist'); + }); + + it('feedback button is not present in the landing page', () => { + cy.getByTestSubj('observabilityOnboardingPageGiveFeedback').should( + 'not.exist' + ); + }); +}); diff --git a/x-pack/plugins/observability_onboarding/e2e/cypress/e2e/logs/system_logs.cy.ts b/x-pack/plugins/observability_onboarding/e2e/cypress/e2e/logs/system_logs.cy.ts new file mode 100644 index 0000000000000..3497ac145eaf3 --- /dev/null +++ b/x-pack/plugins/observability_onboarding/e2e/cypress/e2e/logs/system_logs.cy.ts @@ -0,0 +1,657 @@ +/* + * 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. + */ + +describe('[Logs onboarding] System logs', () => { + describe('System integration', () => { + beforeEach(() => { + cy.deleteIntegration('system'); + }); + + describe('when user is missing privileges', () => { + beforeEach(() => { + cy.loginAsViewerUser(); + cy.visitKibana('/app/observabilityOnboarding/systemLogs'); + }); + + it('installation fails', () => { + cy.getByTestSubj( + 'obltOnboardingSystemLogsIntegrationInstallationFailed' + ).should('exist'); + }); + }); + + describe('when user has proper privileges', () => { + beforeEach(() => { + cy.loginAsEditorUser(); + cy.visitKibana('/app/observabilityOnboarding/systemLogs'); + }); + + after(() => { + cy.deleteIntegration('system'); + }); + + it('installation succeed', () => { + cy.getByTestSubj('obltOnboardingSystemLogsIntegrationInstalled').should( + 'exist' + ); + }); + + it('show link to navigate to system integration when clicking info icon', () => { + cy.getByTestSubj('obltOnboardingSystemLogsIntegrationInstalled').should( + 'exist' + ); + cy.getByTestSubj('obltOnboardingSystemLogsIntegrationInfo') + .should('exist') + .click(); + cy.getByTestSubj( + 'observabilityOnboardingSystemIntegrationLearnMore' + ).should('exist'); + }); + }); + }); + + describe('ApiKey generation', () => { + describe('when user is missing privileges', () => { + it('apiKey is not generated', () => { + cy.loginAsEditorUser(); + cy.visitKibana('/app/observabilityOnboarding/systemLogs'); + + cy.getByTestSubj('obltOnboardingLogsApiKeyCreationNoPrivileges').should( + 'exist' + ); + }); + }); + + describe('when user has proper privileges', () => { + beforeEach(() => { + cy.loginAsLogMonitoringUser(); + cy.visitKibana('/app/observabilityOnboarding/systemLogs'); + }); + + it('apiKey is generated', () => { + cy.getByTestSubj('obltOnboardingLogsApiKeyCreated').should('exist'); + }); + }); + + describe('when an error occurred on creation', () => { + before(() => { + cy.intercept('/internal/observability_onboarding/logs/flow', { + statusCode: 500, + body: { + message: 'Internal error', + }, + }); + + cy.loginAsLogMonitoringUser(); + cy.visitKibana('/app/observabilityOnboarding/systemLogs'); + }); + + it('apiKey is not generated', () => { + cy.getByTestSubj('obltOnboardingLogsApiKeyCreationFailed').should( + 'exist' + ); + }); + }); + }); + + describe('Install the Elastic Agent step', () => { + beforeEach(() => { + cy.intercept('POST', '/internal/observability_onboarding/logs/flow').as( + 'createOnboardingFlow' + ); + cy.loginAsLogMonitoringUser(); + cy.visitKibana('/app/observabilityOnboarding/systemLogs'); + }); + + describe('When user select Linux OS', () => { + it('Auto download config to host is disabled by default', () => { + cy.get('.euiButtonGroup').contains('Linux').click(); + cy.getByTestSubj('obltOnboardingInstallElasticAgentAutoDownloadConfig') + .should('be.enabled') + .should('not.be.checked'); + }); + + it('Installation script is shown', () => { + cy.getByTestSubj('obltOnboardingInstallElasticAgentStep') + .get('.euiCodeBlock') + .should('exist'); + }); + }); + + describe('When user select Mac OS', () => { + beforeEach(() => { + cy.get('.euiButtonGroup').contains('MacOS').click(); + }); + + it('Auto download config to host is disabled by default', () => { + cy.getByTestSubj('obltOnboardingInstallElasticAgentAutoDownloadConfig') + .should('be.enabled') + .should('not.be.checked'); + }); + + it('Installation script is shown', () => { + cy.getByTestSubj('obltOnboardingInstallElasticAgentStep') + .get('.euiCodeBlock') + .should('exist'); + }); + }); + + describe('When user select Windows OS', () => { + beforeEach(() => { + cy.get('.euiButtonGroup').contains('Windows').click(); + }); + + it('Auto download config to host is disabled by default', () => { + cy.getByTestSubj('obltOnboardingInstallElasticAgentAutoDownloadConfig') + .should('be.disabled') + .should('not.be.checked'); + }); + + it('A link to the documentation is shown instead of installation script', () => { + cy.getByTestSubj( + 'obltOnboardingInstallElasticAgentWindowsDocsLink' + ).should('exist'); + + cy.getByTestSubj('obltOnboardingInstallElasticAgentStep') + .get('.euiCodeBlock') + .should('not.exist'); + }); + }); + + describe('When Auto download config', () => { + describe('is selected', () => { + it('autoDownloadConfig flag is added to installation script', () => { + cy.getByTestSubj( + 'obltOnboardingInstallElasticAgentAutoDownloadConfig' + ).click(); + cy.getByTestSubj( + 'obltOnboardingInstallElasticAgentAutoDownloadConfigCallout' + ).should('exist'); + cy.getByTestSubj('obltOnboardingInstallElasticAgentStep') + .get('.euiCodeBlock') + .should('contain', 'autoDownloadConfig=1'); + }); + + it('Download config button is disabled', () => { + cy.getByTestSubj( + 'obltOnboardingInstallElasticAgentAutoDownloadConfig' + ).click(); + cy.getByTestSubj( + 'obltOnboardingConfigureElasticAgentStepDownloadConfig' + ).should('be.disabled'); + }); + }); + + it('is not selected autoDownloadConfig flag is not added to installation script', () => { + cy.getByTestSubj('obltOnboardingInstallElasticAgentStep') + .get('.euiCodeBlock') + .should('not.contain', 'autoDownloadConfig=1'); + }); + }); + + describe('When user executes the installation script in the host', () => { + let onboardingId: string; + + describe('updates on steps are shown in the flow', () => { + beforeEach(() => { + cy.wait('@createOnboardingFlow') + .its('response.body') + .then((body) => { + onboardingId = body.onboardingId; + }); + }); + + describe('Download elastic Agent step', () => { + it('shows a loading callout when elastic agent is downloading', () => { + cy.updateInstallationStepStatus( + onboardingId, + 'ea-download', + 'loading' + ); + cy.getByTestSubj('obltOnboardingStepStatus-loading') + .contains('Downloading Elastic Agent') + .should('exist'); + }); + + it('shows a success callout when elastic agent is downloaded', () => { + cy.updateInstallationStepStatus( + onboardingId, + 'ea-download', + 'complete' + ); + cy.getByTestSubj('obltOnboardingStepStatus-complete') + .contains('Elastic Agent downloaded') + .should('exist'); + }); + + it('shows a danger callout when elastic agent was not downloaded', () => { + cy.updateInstallationStepStatus( + onboardingId, + 'ea-download', + 'danger' + ); + cy.getByTestSubj('obltOnboardingStepStatus-danger') + .contains('Download Elastic Agent') + .should('exist'); + }); + }); + + describe('Extract elastic Agent step', () => { + beforeEach(() => { + cy.updateInstallationStepStatus( + onboardingId, + 'ea-download', + 'complete' + ); + }); + + it('shows a loading callout when elastic agent is extracting', () => { + cy.updateInstallationStepStatus( + onboardingId, + 'ea-extract', + 'loading' + ); + cy.getByTestSubj('obltOnboardingStepStatus-loading') + .contains('Extracting Elastic Agent') + .should('exist'); + }); + + it('shows a success callout when elastic agent is extracted', () => { + cy.updateInstallationStepStatus( + onboardingId, + 'ea-extract', + 'complete' + ); + cy.getByTestSubj('obltOnboardingStepStatus-complete') + .contains('Elastic Agent extracted') + .should('exist'); + }); + + it('shows a danger callout when elastic agent was not extracted', () => { + cy.updateInstallationStepStatus( + onboardingId, + 'ea-extract', + 'danger' + ); + cy.getByTestSubj('obltOnboardingStepStatus-danger') + .contains('Extract Elastic Agent') + .should('exist'); + }); + }); + + describe('Install elastic Agent step', () => { + beforeEach(() => { + cy.updateInstallationStepStatus( + onboardingId, + 'ea-download', + 'complete' + ); + cy.updateInstallationStepStatus( + onboardingId, + 'ea-extract', + 'complete' + ); + }); + + it('shows a loading callout when elastic agent is installing', () => { + cy.updateInstallationStepStatus( + onboardingId, + 'ea-install', + 'loading' + ); + cy.getByTestSubj('obltOnboardingStepStatus-loading') + .contains('Installing Elastic Agent') + .should('exist'); + }); + + it('shows a success callout when elastic agent is installed', () => { + cy.updateInstallationStepStatus( + onboardingId, + 'ea-install', + 'complete' + ); + cy.getByTestSubj('obltOnboardingStepStatus-complete') + .contains('Elastic Agent installed') + .should('exist'); + }); + + it('shows a danger callout when elastic agent was not installed', () => { + cy.updateInstallationStepStatus( + onboardingId, + 'ea-install', + 'danger' + ); + cy.getByTestSubj('obltOnboardingStepStatus-danger') + .contains('Install Elastic Agent') + .should('exist'); + }); + }); + + describe('Check elastic Agent status step', () => { + beforeEach(() => { + cy.updateInstallationStepStatus( + onboardingId, + 'ea-download', + 'complete' + ); + cy.updateInstallationStepStatus( + onboardingId, + 'ea-extract', + 'complete' + ); + cy.updateInstallationStepStatus( + onboardingId, + 'ea-install', + 'complete' + ); + }); + + it('shows a loading callout when getting elastic agent status', () => { + cy.updateInstallationStepStatus( + onboardingId, + 'ea-status', + 'loading' + ); + cy.getByTestSubj('obltOnboardingStepStatus-loading') + .contains('Connecting to the Elastic Agent') + .should('exist'); + }); + + it('shows a success callout when elastic agent status is healthy', () => { + cy.updateInstallationStepStatus( + onboardingId, + 'ea-status', + 'complete' + ); + cy.getByTestSubj('obltOnboardingStepStatus-complete') + .contains('Connected to the Elastic Agent') + .should('exist'); + }); + + it('shows a warning callout when elastic agent status is not healthy', () => { + cy.updateInstallationStepStatus( + onboardingId, + 'ea-status', + 'warning' + ); + cy.getByTestSubj('obltOnboardingStepStatus-warning') + .contains('Connect to the Elastic Agent') + .should('exist'); + }); + }); + }); + }); + }); + + describe('Configure Elastic Agent step', () => { + let onboardingId: string; + + beforeEach(() => { + cy.intercept('POST', '/internal/observability_onboarding/logs/flow').as( + 'createOnboardingFlow' + ); + cy.loginAsLogMonitoringUser(); + cy.visitKibana('/app/observabilityOnboarding/systemLogs'); + cy.wait('@createOnboardingFlow') + .its('response.body') + .then((body) => { + onboardingId = body.onboardingId; + }); + }); + + describe('When user select Linux OS', () => { + beforeEach(() => { + cy.getByTestSubj( + 'obltOnboardingInstallElasticAgentAutoDownloadConfig' + ).click(); + cy.updateInstallationStepStatus( + onboardingId, + 'ea-download', + 'complete' + ); + cy.updateInstallationStepStatus(onboardingId, 'ea-extract', 'complete'); + cy.updateInstallationStepStatus(onboardingId, 'ea-install', 'complete'); + cy.updateInstallationStepStatus(onboardingId, 'ea-status', 'complete'); + }); + + it('shows loading callout when config is being downloaded to the host', () => { + cy.updateInstallationStepStatus(onboardingId, 'ea-config', 'loading'); + cy.get( + '[data-test-subj="obltOnboardingConfigureElasticAgentStep"] .euiStep__titleWrapper [class$="euiStepNumber-s-loading"]' + ).should('exist'); + cy.getByTestSubj('obltOnboardingStepStatus-loading') + .contains('Downloading Elastic Agent config') + .should('exist'); + }); + + it('shows success callout when the configuration has been written to the host', () => { + cy.updateInstallationStepStatus(onboardingId, 'ea-config', 'complete'); + cy.get( + '[data-test-subj="obltOnboardingConfigureElasticAgentStep"] .euiStep__titleWrapper [class$="euiStepNumber-s-complete"]' + ).should('exist'); + cy.getByTestSubj('obltOnboardingStepStatus-complete') + .contains( + 'Elastic Agent config written to /opt/Elastic/Agent/elastic-agent.yml' + ) + .should('exist'); + }); + + it('shows warning callout when the configuration was not written in the host', () => { + cy.updateInstallationStepStatus(onboardingId, 'ea-config', 'warning'); + cy.get( + '[data-test-subj="obltOnboardingConfigureElasticAgentStep"] .euiStep__titleWrapper [class$="euiStepNumber-s-warning"]' + ).should('exist'); + cy.getByTestSubj('obltOnboardingStepStatus-warning') + .contains('Configure the agent') + .should('exist'); + }); + }); + + describe('When user select Mac OS', () => { + beforeEach(() => { + cy.get('.euiButtonGroup').contains('MacOS').click(); + cy.getByTestSubj( + 'obltOnboardingInstallElasticAgentAutoDownloadConfig' + ).click(); + cy.updateInstallationStepStatus( + onboardingId, + 'ea-download', + 'complete' + ); + cy.updateInstallationStepStatus(onboardingId, 'ea-extract', 'complete'); + cy.updateInstallationStepStatus(onboardingId, 'ea-install', 'complete'); + cy.updateInstallationStepStatus(onboardingId, 'ea-status', 'complete'); + }); + + it('shows loading callout when config is being downloaded to the host', () => { + cy.updateInstallationStepStatus(onboardingId, 'ea-config', 'loading'); + cy.get( + '[data-test-subj="obltOnboardingConfigureElasticAgentStep"] .euiStep__titleWrapper [class$="euiStepNumber-s-loading"]' + ).should('exist'); + cy.getByTestSubj('obltOnboardingStepStatus-loading') + .contains('Downloading Elastic Agent config') + .should('exist'); + }); + + it('shows success callout when the configuration has been written to the host', () => { + cy.updateInstallationStepStatus(onboardingId, 'ea-config', 'complete'); + cy.get( + '[data-test-subj="obltOnboardingConfigureElasticAgentStep"] .euiStep__titleWrapper [class$="euiStepNumber-s-complete"]' + ).should('exist'); + cy.getByTestSubj('obltOnboardingStepStatus-complete') + .contains( + 'Elastic Agent config written to /Library/Elastic/Agent/elastic-agent.yml' + ) + .should('exist'); + }); + + it('shows warning callout when the configuration was not written in the host', () => { + cy.updateInstallationStepStatus(onboardingId, 'ea-config', 'warning'); + cy.get( + '[data-test-subj="obltOnboardingConfigureElasticAgentStep"] .euiStep__titleWrapper [class$="euiStepNumber-s-warning"]' + ).should('exist'); + cy.getByTestSubj('obltOnboardingStepStatus-warning') + .contains('Configure the agent') + .should('exist'); + }); + }); + + describe('When user select Windows', () => { + beforeEach(() => { + cy.get('.euiButtonGroup').contains('Windows').click(); + }); + + it('step is disabled', () => { + cy.get( + '[data-test-subj="obltOnboardingConfigureElasticAgentStep"] .euiStep__titleWrapper [class$="euiStepNumber-s-disabled"]' + ).should('exist'); + }); + }); + }); + + describe('Check logs step', () => { + let onboardingId: string; + + beforeEach(() => { + cy.intercept('POST', '/internal/observability_onboarding/logs/flow').as( + 'createOnboardingFlow' + ); + cy.loginAsLogMonitoringUser(); + cy.visitKibana('/app/observabilityOnboarding/systemLogs'); + cy.wait('@createOnboardingFlow') + .its('response.body') + .then((body) => { + onboardingId = body.onboardingId; + }); + }); + + describe('When user select Linux OS or MacOS', () => { + describe('When configure Elastic Agent step is not finished', () => { + beforeEach(() => { + cy.updateInstallationStepStatus( + onboardingId, + 'ea-download', + 'complete' + ); + cy.updateInstallationStepStatus( + onboardingId, + 'ea-extract', + 'complete' + ); + cy.updateInstallationStepStatus( + onboardingId, + 'ea-install', + 'complete' + ); + cy.updateInstallationStepStatus(onboardingId, 'ea-status', 'loading'); + }); + + it('check logs is not triggered', () => { + cy.get( + '[data-test-subj="obltOnboardingCheckLogsStep"] .euiStep__titleWrapper [class$="euiStepNumber-s-incomplete"]' + ).should('exist'); + cy.get('.euiStep__title') + .contains('Ship logs to Elastic Observability') + .should('exist'); + }); + }); + + describe('When configure Elastic Agent step has finished', () => { + beforeEach(() => { + cy.updateInstallationStepStatus( + onboardingId, + 'ea-download', + 'complete' + ); + cy.updateInstallationStepStatus( + onboardingId, + 'ea-extract', + 'complete' + ); + cy.updateInstallationStepStatus( + onboardingId, + 'ea-install', + 'complete' + ); + cy.updateInstallationStepStatus( + onboardingId, + 'ea-status', + 'complete' + ); + cy.updateInstallationStepStatus( + onboardingId, + 'ea-config', + 'complete' + ); + }); + + it('shows loading callout when logs are being checked', () => { + cy.get( + '[data-test-subj="obltOnboardingCheckLogsStep"] .euiStep__titleWrapper [class$="euiStepNumber-s-loading"]' + ).should('exist'); + cy.get('.euiStep__title') + .contains('Waiting for logs to be shipped...') + .should('exist'); + }); + }); + }); + + describe('When user select Windows', () => { + beforeEach(() => { + cy.get('.euiButtonGroup').contains('Windows').click(); + }); + + it('step is disabled', () => { + cy.get( + '[data-test-subj="obltOnboardingCheckLogsStep"] .euiStep__titleWrapper [class$="euiStepNumber-s-disabled"]' + ).should('exist'); + }); + }); + }); + + describe('When logs are being shipped', () => { + beforeEach(() => { + cy.intercept('GET', '**/progress', { + status: 200, + body: { + progress: { + 'ea-download': { status: 'complete' }, + 'ea-extract': { status: 'complete' }, + 'ea-install': { status: 'complete' }, + 'ea-status': { status: 'complete' }, + 'ea-config': { status: 'complete' }, + 'logs-ingest': { status: 'complete' }, + }, + }, + }).as('checkOnboardingProgress'); + cy.intercept('GET', '/api/fleet/epm/packages/system').as( + 'systemIntegrationInstall' + ); + cy.loginAsLogMonitoringUser(); + cy.visitKibana('/app/observabilityOnboarding/systemLogs'); + }); + + it('shows success callout when logs has arrived to elastic', () => { + cy.wait('@checkOnboardingProgress'); + cy.get( + '[data-test-subj="obltOnboardingCheckLogsStep"] .euiStep__titleWrapper [class$="euiStepNumber-s-complete"]' + ).should('exist'); + cy.get('.euiStep__title') + .contains('Logs are being shipped!') + .should('exist'); + }); + + it('when user clicks on Explore Logs it navigates to discover', () => { + cy.wait('@systemIntegrationInstall'); + cy.wait('@checkOnboardingProgress'); + cy.getByTestSubj('obltOnboardingExploreLogs').should('exist').click(); + cy.url().should('include', '/app/discover'); + + cy.get('button[title="logs-*"]').should('exist'); + }); + }); +}); diff --git a/x-pack/plugins/observability_onboarding/e2e/cypress/e2e/navigation.cy.ts b/x-pack/plugins/observability_onboarding/e2e/cypress/e2e/navigation.cy.ts new file mode 100644 index 0000000000000..cc5d0e2500ad1 --- /dev/null +++ b/x-pack/plugins/observability_onboarding/e2e/cypress/e2e/navigation.cy.ts @@ -0,0 +1,62 @@ +/* + * 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. + */ + +describe('[Observability onboarding] Navigation', () => { + beforeEach(() => { + cy.loginAsElastic(); + cy.visitKibana('/app/observabilityOnboarding/'); + }); + + describe('When user clicks on the card', () => { + it('navigates to system logs onboarding', () => { + cy.getByTestSubj('obltOnboardingHomeStartSystemLogStream').click(); + + cy.url().should('include', '/app/observabilityOnboarding/systemLogs'); + }); + + it('navigates to custom logs onboarding', () => { + cy.getByTestSubj('obltOnboardingHomeStartLogFileStream').click(); + + cy.url().should('include', '/app/observabilityOnboarding/customLogs'); + }); + + it('navigates to apm tutorial', () => { + cy.getByTestSubj('obltOnboardingHomeStartApmTutorial').click(); + + cy.url().should('include', '/app/home#/tutorial/apm'); + }); + + it('navigates to kubernetes integration', () => { + cy.getByTestSubj('obltOnboardingHomeGoToKubernetesIntegration').click(); + + cy.url().should( + 'include', + '/app/integrations/detail/kubernetes/overview' + ); + }); + + it('navigates to integrations', () => { + cy.getByTestSubj('obltOnboardingHomeExploreIntegrations').click(); + + cy.url().should('include', '/app/integrations/browse'); + }); + }); + + describe('When user clicks on Quick links', () => { + it('navigates to use sample data', () => { + cy.getByTestSubj('obltOnboardingHomeUseSampleData').click(); + + cy.url().should('include', '/app/home#/tutorial_directory/sampleData'); + }); + + it('navigates to upload a file', () => { + cy.getByTestSubj('obltOnboardingHomeUploadAFile').click(); + + cy.url().should('include', '/app/home#/tutorial_directory/fileDataViz'); + }); + }); +}); diff --git a/x-pack/plugins/observability_onboarding/e2e/cypress/support/commands.ts b/x-pack/plugins/observability_onboarding/e2e/cypress/support/commands.ts index 299729f1d8fa7..ea321f48a0bae 100644 --- a/x-pack/plugins/observability_onboarding/e2e/cypress/support/commands.ts +++ b/x-pack/plugins/observability_onboarding/e2e/cypress/support/commands.ts @@ -6,6 +6,51 @@ */ import URL from 'url'; +import { ObservabilityOnboardingUsername } from '../../../server/test_helpers/create_observability_onboarding_users/authentication'; + +export type InstallationStep = + | 'ea-download' + | 'ea-extract' + | 'ea-install' + | 'ea-status' + | 'ea-config'; + +export type InstallationStepStatus = + | 'incomplete' + | 'complete' + | 'disabled' + | 'loading' + | 'warning' + | 'danger' + | 'current'; + +Cypress.Commands.add('loginAsViewerUser', () => { + return cy.loginAs({ + username: ObservabilityOnboardingUsername.viewerUser, + password: 'changeme', + }); +}); + +Cypress.Commands.add('loginAsEditorUser', () => { + return cy.loginAs({ + username: ObservabilityOnboardingUsername.editorUser, + password: 'changeme', + }); +}); + +Cypress.Commands.add('loginAsLogMonitoringUser', () => { + return cy.loginAs({ + username: ObservabilityOnboardingUsername.logMonitoringUser, + password: 'changeme', + }); +}); + +Cypress.Commands.add('loginAsElastic', () => { + return cy.loginAs({ + username: 'elastic', + password: 'changeme', + }); +}); Cypress.Commands.add( 'loginAs', @@ -31,13 +76,6 @@ Cypress.Commands.add( } ); -Cypress.Commands.add('loginAsElastic', () => { - return cy.loginAs({ - username: 'elastic', - password: 'changeme', - }); -}); - Cypress.Commands.add('getByTestSubj', (selector: string) => { return cy.get(`[data-test-subj="${selector}"]`); }); @@ -57,3 +95,59 @@ Cypress.Commands.add( }); } ); + +Cypress.Commands.add('deleteIntegration', (integrationName: string) => { + const kibanaUrl = Cypress.env('KIBANA_URL'); + + cy.request({ + log: false, + method: 'GET', + url: `${kibanaUrl}/api/fleet/epm/packages/${integrationName}`, + headers: { + 'kbn-xsrf': 'e2e_test', + }, + auth: { user: 'editor', pass: 'changeme' }, + }).then((response) => { + const status = response.body.item.status; + if (status === 'installed') { + cy.request({ + log: false, + method: 'DELETE', + url: `${kibanaUrl}/api/fleet/epm/packages/${integrationName}`, + body: { + force: false, + }, + headers: { + 'kbn-xsrf': 'e2e_test', + }, + auth: { user: 'editor', pass: 'changeme' }, + }); + } + }); +}); + +Cypress.Commands.add( + 'updateInstallationStepStatus', + ( + onboardingId: string, + step: InstallationStep, + status: InstallationStepStatus + ) => { + const kibanaUrl = Cypress.env('KIBANA_URL'); + + cy.log(onboardingId, step, status); + + cy.request({ + log: false, + method: 'POST', + url: `${kibanaUrl}/internal/observability_onboarding/flow/${onboardingId}/step/${step}`, + headers: { + 'kbn-xsrf': 'e2e_test', + }, + auth: { user: 'editor', pass: 'changeme' }, + body: { + status, + }, + }); + } +); diff --git a/x-pack/plugins/observability_onboarding/e2e/cypress/support/types.d.ts b/x-pack/plugins/observability_onboarding/e2e/cypress/support/types.d.ts index 8db14ad70561a..6b5695d159d76 100644 --- a/x-pack/plugins/observability_onboarding/e2e/cypress/support/types.d.ts +++ b/x-pack/plugins/observability_onboarding/e2e/cypress/support/types.d.ts @@ -11,8 +11,17 @@ declare namespace Cypress { username: string; password: string; }): Cypress.Chainable>; + loginAsViewerUser(): Cypress.Chainable>; + loginAsEditorUser(): Cypress.Chainable>; + loginAsLogMonitoringUser(): Cypress.Chainable>; loginAsElastic(): Cypress.Chainable>; getByTestSubj(selector: string): Chainable>; visitKibana(url: string, rangeFrom?: string, rangeTo?: string): void; + deleteIntegration(integrationName: string): void; + updateInstallationStepStatus( + onboardingId: string, + step: InstallationStep, + status: InstallationStepStatus + ): void; } } diff --git a/x-pack/plugins/observability_onboarding/e2e/cypress_test_runner.ts b/x-pack/plugins/observability_onboarding/e2e/cypress_test_runner.ts index 1d056897a6d02..34765a892cc1c 100644 --- a/x-pack/plugins/observability_onboarding/e2e/cypress_test_runner.ts +++ b/x-pack/plugins/observability_onboarding/e2e/cypress_test_runner.ts @@ -9,6 +9,7 @@ import cypress from 'cypress'; import path from 'path'; import Url from 'url'; import { FtrProviderContext } from './ftr_provider_context'; +import { createObservabilityOnboardingUsers } from '../server/test_helpers/create_observability_onboarding_users'; export async function cypressTestRunner({ ftrProviderContext: { getService }, @@ -22,6 +23,13 @@ export async function cypressTestRunner({ const username = config.get('servers.elasticsearch.username'); const password = config.get('servers.elasticsearch.password'); + const kibanaUrl = Url.format({ + protocol: config.get('servers.kibana.protocol'), + hostname: config.get('servers.kibana.hostname'), + port: config.get('servers.kibana.port'), + auth: `${username}:${password}`, + }); + const esNode = Url.format({ protocol: config.get('servers.elasticsearch.protocol'), port: config.get('servers.elasticsearch.port'), @@ -29,6 +37,12 @@ export async function cypressTestRunner({ auth: `${username}:${password}`, }); + // Creates ObservabilityOnboarding users + await createObservabilityOnboardingUsers({ + elasticsearch: { node: esNode, username, password }, + kibana: { hostname: kibanaUrl }, + }); + const esRequestTimeout = config.get('timeouts.esRequestTimeout'); const kibanaUrlWithoutAuth = Url.format({ diff --git a/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/wizard/api_key_banner.tsx b/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/wizard/api_key_banner.tsx index 57a7ba7b4cab1..c55f554fc4845 100644 --- a/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/wizard/api_key_banner.tsx +++ b/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/wizard/api_key_banner.tsx @@ -54,6 +54,7 @@ export function ApiKeyBanner({ } color="primary" + data-test-subj="obltOnboardingLogsCreatingApiKey" /> ); @@ -67,6 +68,7 @@ export function ApiKeyBanner({ )} color="success" iconType="check" + data-test-subj="obltOnboardingLogsApiKeyCreated" >

{i18n.translate( @@ -123,6 +125,7 @@ export function ApiKeyBanner({ )} color="danger" iconType="error" + data-test-subj="obltOnboardingLogsApiKeyCreationFailed" >

{i18n.translate( @@ -148,6 +151,7 @@ export function ApiKeyBanner({ )} color="warning" iconType="warning" + data-test-subj="obltOnboardingLogsApiKeyCreationNoPrivileges" >

{i18n.translate( diff --git a/x-pack/plugins/observability_onboarding/public/components/app/home/index.tsx b/x-pack/plugins/observability_onboarding/public/components/app/home/index.tsx index 27a44e94f07b6..6dcb343b222cb 100644 --- a/x-pack/plugins/observability_onboarding/public/components/app/home/index.tsx +++ b/x-pack/plugins/observability_onboarding/public/components/app/home/index.tsx @@ -123,7 +123,12 @@ export function Home() { { defaultMessage: 'Stream host system logs' } )} footer={ - + {getStartedLabel} } @@ -216,7 +221,11 @@ export function Home() { } )} footer={ - + {getStartedLabel} } @@ -245,6 +254,7 @@ export function Home() { {getStartedLabel} @@ -282,7 +292,11 @@ export function Home() { )} footer={ <> - + {i18n.translate( 'xpack.observability_onboarding.card.integrations.start', { defaultMessage: 'Start exploring' } @@ -298,14 +312,20 @@ export function Home() { - + {i18n.translate( 'xpack.observability_onboarding.card.integrations.sampleData', { defaultMessage: 'Use sample data' } )} - + {i18n.translate( 'xpack.observability_onboarding.card.integrations.uploadFile', { defaultMessage: 'Upload a file' } diff --git a/x-pack/plugins/observability_onboarding/public/components/app/system_logs/install_elastic_agent.tsx b/x-pack/plugins/observability_onboarding/public/components/app/system_logs/install_elastic_agent.tsx index 5a71c588c0711..4c7e7fd63625e 100644 --- a/x-pack/plugins/observability_onboarding/public/components/app/system_logs/install_elastic_agent.tsx +++ b/x-pack/plugins/observability_onboarding/public/components/app/system_logs/install_elastic_agent.tsx @@ -171,7 +171,11 @@ export function InstallElasticAgent() { : stepStatus === 'complete' ? CHECK_LOGS_LABELS.completed : CHECK_LOGS_LABELS.incomplete; - return { title, status: stepStatus }; + return { + title, + status: stepStatus, + 'data-test-subj': 'obltOnboardingCheckLogsStep', + }; } return { title: CHECK_LOGS_LABELS.incomplete, @@ -203,6 +207,7 @@ export function InstallElasticAgent() { fill iconType="magnifyWithPlus" onClick={onContinue} + data-test-subj="obltOnboardingExploreLogs" > {i18n.translate( 'xpack.observability_onboarding.steps.exploreLogs', diff --git a/x-pack/plugins/observability_onboarding/public/components/app/system_logs/system_integration_banner.tsx b/x-pack/plugins/observability_onboarding/public/components/app/system_logs/system_integration_banner.tsx index d567a13f3d3a4..61ebaca8056ea 100644 --- a/x-pack/plugins/observability_onboarding/public/components/app/system_logs/system_integration_banner.tsx +++ b/x-pack/plugins/observability_onboarding/public/components/app/system_logs/system_integration_banner.tsx @@ -74,6 +74,7 @@ export function SystemIntegrationBanner() { } color="primary" + data-test-subj="obltOnboardingSystemLogsInstallingIntegration" /> ); } @@ -89,6 +90,7 @@ export function SystemIntegrationBanner() { )} color="warning" iconType="warning" + data-test-subj="obltOnboardingSystemLogsIntegrationInstallationFailed" > {error?.message} @@ -106,6 +108,7 @@ export function SystemIntegrationBanner() { values={{ systemIntegrationTooltip: ( ); diff --git a/x-pack/plugins/observability_onboarding/public/components/shared/install_elastic_agent_steps.tsx b/x-pack/plugins/observability_onboarding/public/components/shared/install_elastic_agent_steps.tsx index 16996a18d05b0..282591e0a1487 100644 --- a/x-pack/plugins/observability_onboarding/public/components/shared/install_elastic_agent_steps.tsx +++ b/x-pack/plugins/observability_onboarding/public/components/shared/install_elastic_agent_steps.tsx @@ -111,12 +111,7 @@ export function InstallElasticAgentSteps({ selectedPlatform ); return ( - + ); })} @@ -184,6 +179,7 @@ export function InstallElasticAgentSteps({ download="elastic-agent.yml" target="_blank" isDisabled={autoDownloadConfig} + data-test-subj="obltOnboardingConfigureElasticAgentStepDownloadConfig" > {i18n.translate( 'xpack.observability_onboarding.installElasticAgent.configStep.downloadConfigButton', @@ -209,6 +205,7 @@ export function InstallElasticAgentSteps({ ({ checked={autoDownloadConfig} onChange={onToggleAutoDownloadConfig} disabled={disableSteps || isInstallStarted} + data-test-subj="obltOnboardingInstallElasticAgentAutoDownloadConfig" /> {autoDownloadConfig && ( @@ -288,6 +286,7 @@ export function InstallElasticAgentSteps({ )} color="warning" iconType="warning" + data-test-subj="obltOnboardingInstallElasticAgentAutoDownloadConfigCallout" /> @@ -318,6 +317,7 @@ export function InstallElasticAgentSteps({ ), }, { + 'data-test-subj': 'obltOnboardingConfigureElasticAgentStep', title: i18n.translate( 'xpack.observability_onboarding.installElasticAgent.configureStep.title', { defaultMessage: 'Configure the Elastic agent' } @@ -329,6 +329,7 @@ export function InstallElasticAgentSteps({ children: null, ...euiStep, status: disableSteps ? 'disabled' : euiStep.status, + 'data-test-subj': euiStep['data-test-subj'], })), ]} /> diff --git a/x-pack/plugins/observability_onboarding/public/components/shared/popover_tooltip.tsx b/x-pack/plugins/observability_onboarding/public/components/shared/popover_tooltip.tsx index 66165edc8e133..8a0e8b53e7350 100644 --- a/x-pack/plugins/observability_onboarding/public/components/shared/popover_tooltip.tsx +++ b/x-pack/plugins/observability_onboarding/public/components/shared/popover_tooltip.tsx @@ -13,6 +13,7 @@ interface PopoverTooltipProps { iconType?: string; title?: string; children: React.ReactNode; + dataTestSubj: string; } export function PopoverTooltip({ @@ -20,6 +21,7 @@ export function PopoverTooltip({ iconType = 'iInCircle', title, children, + dataTestSubj, }: PopoverTooltipProps) { const [isPopoverOpen, setIsPopoverOpen] = useState(false); @@ -32,6 +34,7 @@ export function PopoverTooltip({ style={{ margin: '-5px 0 0 -5px' }} button={ ) => { setIsPopoverOpen(!isPopoverOpen); diff --git a/x-pack/plugins/observability_onboarding/public/components/shared/step_status.tsx b/x-pack/plugins/observability_onboarding/public/components/shared/step_status.tsx index 59ad5d202cf89..d2a5341fda731 100644 --- a/x-pack/plugins/observability_onboarding/public/components/shared/step_status.tsx +++ b/x-pack/plugins/observability_onboarding/public/components/shared/step_status.tsx @@ -27,7 +27,7 @@ export function StepStatus({ }) { if (status === 'loading') { return ( - + @@ -43,7 +43,7 @@ export function StepStatus({ } if (status === 'complete') { return ( - + {message} @@ -52,7 +52,7 @@ export function StepStatus({ } if (status === 'danger') { return ( - + {message} @@ -61,7 +61,7 @@ export function StepStatus({ } if (status === 'warning') { return ( - + {message} diff --git a/x-pack/plugins/observability_onboarding/public/components/shared/windows_install_step.tsx b/x-pack/plugins/observability_onboarding/public/components/shared/windows_install_step.tsx index 42677924a4706..6784095d4e516 100644 --- a/x-pack/plugins/observability_onboarding/public/components/shared/windows_install_step.tsx +++ b/x-pack/plugins/observability_onboarding/public/components/shared/windows_install_step.tsx @@ -40,6 +40,7 @@ export function WindowsInstallStep({ href={docsLink} target="_blank" style={{ width: 'fit-content' }} + data-test-subj="obltOnboardingInstallElasticAgentWindowsDocsLink" > {i18n.translate( 'xpack.observability_onboarding.windows.installStep.link.label', diff --git a/x-pack/test/apm_api_integration/tests/sourcemaps/sourcemaps.ts b/x-pack/test/apm_api_integration/tests/sourcemaps/sourcemaps.ts index 769041847bb3f..2b02096f57d39 100644 --- a/x-pack/test/apm_api_integration/tests/sourcemaps/sourcemaps.ts +++ b/x-pack/test/apm_api_integration/tests/sourcemaps/sourcemaps.ts @@ -27,16 +27,6 @@ const SAMPLE_SOURCEMAP = { mappings: 'A,AAAB;;ABCDE;', }; -const SAMPLE_ANDROID_MAP = `# compiler: R8 -# compiler_version: 3.2.47 -# min_api: 26 -# common_typos_disable -# {"id":"com.android.tools.r8.mapping","version":"2.0"} -# pg_map_id: 127b14c -# pg_map_hash: SHA-256 127b14c0be5dd1b55beee544a8d0e7c9414b432868ed8bc54ca5cc43cba12435 -a1.TableInfo$ForeignKey$$ExternalSyntheticOutline0 -> a1.e: -# {"id":"sourceFile","fileName":"R8$$SyntheticClass"}`; - export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); @@ -109,28 +99,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { return response.body; } - async function uploadAndroidMap({ - serviceName, - serviceVersion, - androidMap, - }: { - serviceName: string; - serviceVersion: string; - androidMap: string; - }) { - const response = await apmApiClient.writeUser({ - endpoint: 'POST /api/apm/androidmaps 2023-10-31', - type: 'form-data', - params: { - body: { - service_name: serviceName, - service_version: serviceVersion, - map_file: androidMap, - }, - }, - }); - return response.body; - } async function runSourceMapMigration() { await apmApiClient.writeUser({ endpoint: 'POST /internal/apm/sourcemaps/migrate_fleet_artifacts', @@ -160,12 +128,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { await Promise.all([deleteAllFleetSourceMaps(), deleteAllApmSourceMaps()]); }); - async function getDecodedMapContent(encodedContent?: string): Promise { - if (encodedContent) { - return (await unzip(Buffer.from(encodedContent, 'base64'))).toString(); - } - } - async function getDecodedSourceMapContent( encodedContent?: string ): Promise { @@ -281,111 +243,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); }); - let androidResp: APIReturnType<'POST /api/apm/androidmaps 2023-10-31'>; - describe('upload android map', () => { - after(async () => { - await apmApiClient.writeUser({ - endpoint: 'DELETE /api/apm/sourcemaps/{id} 2023-10-31', - params: { path: { id: androidResp.id } }, - }); - }); - - before(async () => { - androidResp = await uploadAndroidMap({ - serviceName: 'uploading-test', - serviceVersion: '1.0.0', - androidMap: SAMPLE_ANDROID_MAP, - }); - - await waitForSourceMapCount(1); - }); - - it('is uploaded as a fleet artifact', async () => { - const res = await es.search({ - index: '.fleet-artifacts', - size: 1, - query: { - bool: { - filter: [{ term: { type: 'sourcemap' } }, { term: { package_name: 'apm' } }], - }, - }, - }); - - // @ts-expect-error - expect(res.hits.hits[0]._source.identifier).to.be('uploading-test-1.0.0-android'); - }); - - it('is added to .apm-source-map index', async () => { - const res = await es.search({ - index: '.apm-source-map', - }); - - const source = res.hits.hits[0]._source; - const decodedSourceMap = await getDecodedMapContent(source?.content); - expect(decodedSourceMap).to.eql(SAMPLE_ANDROID_MAP); - expect(source?.content_sha256).to.be( - '702e07279b0fbed47fdbf5e71528dff845b4f07a16ca79cab0c1b06eb71be966' - ); - expect(source?.file.path).to.be('android'); - expect(source?.service.name).to.be('uploading-test'); - expect(source?.service.version).to.be('1.0.0'); - }); - - describe('when uploading a new android map with the same service.name and service.version', () => { - let resBefore: GetResponse; - let resAfter: GetResponse; - - before(async () => { - async function getSourceMapDocFromApmIndex() { - await es.indices.refresh({ index: '.apm-source-map' }); - return await es.get({ - index: '.apm-source-map', - id: 'uploading-test-1.0.0-android', - }); - } - - resBefore = await getSourceMapDocFromApmIndex(); - - await uploadAndroidMap({ - serviceName: 'uploading-test', - serviceVersion: '1.0.0', - androidMap: '# compiler: R8\n# ANOTHER MAP', - }); - - resAfter = await getSourceMapDocFromApmIndex(); - }); - - after(async () => { - await deleteAllApmSourceMaps(); - await deleteAllFleetSourceMaps(); - }); - - it('creates one document in the .apm-source-map index', async () => { - const res = await es.search({ index: '.apm-source-map', size: 0 }); - - // @ts-expect-error - expect(res.hits.total.value).to.be(1); - }); - - it('creates two documents in the .fleet-artifacts index', async () => { - const res = await listSourcemaps({ page: 1, perPage: 10 }); - expect(res.total).to.be(2); - }); - - it('updates the content', async () => { - const contentBefore = await getDecodedMapContent(resBefore._source?.content); - const contentAfter = await getDecodedMapContent(resAfter._source?.content); - - expect(contentBefore).to.be(SAMPLE_ANDROID_MAP); - expect(contentAfter).to.be('# compiler: R8\n# ANOTHER MAP'); - }); - - it('updates the content hash', async () => { - expect(resBefore._source?.content_sha256).to.not.be(resAfter._source?.content_sha256); - }); - }); - }); - describe('list source maps', async () => { before(async () => { const totalCount = 6; diff --git a/x-pack/test_serverless/api_integration/test_suites/common/scripted_fields.ts b/x-pack/test_serverless/api_integration/test_suites/common/scripted_fields.ts index f3969e07eea7e..7cc4089814c77 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/scripted_fields.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/scripted_fields.ts @@ -13,7 +13,8 @@ export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - describe('scripted fields disabled', function () { + // FLAKY: https://github.com/elastic/kibana/issues/165511 + describe.skip('scripted fields disabled', function () { before(async () => { await esArchiver.load('test/api_integration/fixtures/es_archiver/index_patterns/basic_index'); });