[Observability Onboarding] Add telemetry events for logs onboarding flows (#180439)

Closes https://github.com/elastic/kibana/issues/179555
Closes https://github.com/elastic/kibana/issues/179786

Depends on: https://github.com/elastic/kibana/pull/180301
> [!NOTE]  
> The current flow in `main` is a bit broken and does not report the
final completed status. Make sure to wait until the above PR is merged
or cherry pick its commit before testing.

## Summary

* Adds a new schema for the `observability_onboarding` event
* Adds logic to trigger the event on the onboarding landing pages (old
and new)
* Adds logic to trigger the event during the system/custom logs flows
when: user has downloaded the agent, when agent has reported it's
status, in case of warning/errors and finally when the flow has been
completed.

## How to test

* Run run serverless Kibana localy
* Set the new onboarding feature flag on or off depending on which one
you want to test:
```
# kibana.dev.yml
xpack.cloud_integrations.experiments.enabled: true
xpack.cloud_integrations.experiments.flag_overrides:
  "observability_onboarding.experimental_onboarding_flow_enabled": true
```
* (Annoying workaround 🙈) In order to make Elastic Agent to communicate
with ES over https, modify `outputs.default` configuration in
`x-pack/plugins/observability_solution/observability_onboarding/common/elastic_agent_logs/system_logs/generate_system_logs_yml.ts`
and
`x-pack/plugins/observability_solution/observability_onboarding/common/elastic_agent_logs/custom_logs/generate_custom_logs_yml.ts`
to use your local Kiabana SSL certificate:
```
outputs: {
  default: {
    ...
    ssl: {
      enabled: true,
      certificate_authorities: [
        // Replace with you local path to Kibana repo
        '/Users/mykolaharmash/Developer/kibana/packages/kbn-dev-utils/certs/ca.crt',
      ],
    },
  }
}
```
* Go trough the onboarding flow and make sure you see `/kibana-browser`
requests in the "Network" with the correct payload.

---------

Co-authored-by: Thom Heymann <190132+thomheymann@users.noreply.github.com>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Mykola Harmash 2024-04-12 18:11:04 +02:00 committed by GitHub
parent ce14d99f3b
commit 891358ff80
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 299 additions and 20 deletions

View file

@ -0,0 +1,15 @@
/*
* 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.
*/
export type EaInstallProgressStepId =
| 'ea-download'
| 'ea-extract'
| 'ea-install'
| 'ea-status'
| 'ea-config';
export type LogsFlowProgressStepId = EaInstallProgressStepId | 'logs-ingest';

View file

@ -0,0 +1,57 @@
/*
* 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 { type EventTypeOpts } from '@kbn/analytics-client';
export const OBSERVABILITY_ONBOARDING_TELEMETRY_EVENT: EventTypeOpts<{
flow?: string;
step?: string;
step_status?: string;
step_message?: string;
uses_legacy_onboarding_page: boolean;
}> = {
eventType: 'observability_onboarding',
schema: {
flow: {
type: 'keyword',
_meta: {
description:
"The current onboarding flow user is going through (e.g. 'system_logs', 'nginx'). If not present, user is on the landing screen.",
optional: true,
},
},
step: {
type: 'keyword',
_meta: {
description: 'The current step in the onboarding flow.',
optional: true,
},
},
step_status: {
type: 'keyword',
_meta: {
description: 'The status of the step in the onboarding flow.',
optional: true,
},
},
step_message: {
type: 'keyword',
_meta: {
description:
'Error or warning message of the current step in the onboarding flow',
optional: true,
},
},
uses_legacy_onboarding_page: {
type: 'boolean',
_meta: {
description:
'Whether the user is using the legacy onboarding page or the new one',
},
},
},
};

View file

@ -24,6 +24,7 @@ import { Router, Routes, Route } from '@kbn/shared-ux-router';
import { euiDarkVars, euiLightVars } from '@kbn/ui-theme';
import React from 'react';
import ReactDOM from 'react-dom';
import { OBSERVABILITY_ONBOARDING_TELEMETRY_EVENT } from '../../common/telemetry_events';
import { ConfigSchema } from '..';
import { customLogsRoutes } from '../components/app/custom_logs';
import { systemLogsRoutes } from '../components/app/system_logs';
@ -36,6 +37,7 @@ import { baseRoutes, routes } from '../routes';
import { CustomLogs } from '../routes/templates/custom_logs';
import { SystemLogs } from '../routes/templates/system_logs';
import { ExperimentalOnboardingFlow } from './experimental_onboarding_flow';
import { ExperimentalOnboardingFeatureFlag } from '../context/experimental_onboarding_enabled';
export const onBoardingTitle = i18n.translate(
'xpack.observability_onboarding.breadcrumbs.onboarding',
@ -145,6 +147,13 @@ export function ObservabilityOnboardingAppRoot({
const renderFeedbackLinkAsPortal = !config.serverless.enabled;
core.analytics.reportEvent(
OBSERVABILITY_ONBOARDING_TELEMETRY_EVENT.eventType,
{
uses_legacy_onboarding_page: !experimentalOnboardingFlowEnabled,
}
);
return (
<div className={APP_WRAPPER_CLASS}>
<RedirectAppLinks
@ -181,11 +190,15 @@ export function ObservabilityOnboardingAppRoot({
<ObservabilityOnboardingHeaderActionMenu />
</HeaderMenuPortal>
)}
{experimentalOnboardingFlowEnabled ? (
<ExperimentalOnboardingFlow />
) : (
<ObservabilityOnboardingApp />
)}
<ExperimentalOnboardingFeatureFlag.Provider
value={experimentalOnboardingFlowEnabled}
>
{experimentalOnboardingFlowEnabled ? (
<ExperimentalOnboardingFlow />
) : (
<ObservabilityOnboardingApp />
)}
</ExperimentalOnboardingFeatureFlag.Provider>
</EuiErrorBoundary>
</Router>
</i18nCore.Context>

View file

@ -4,6 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import React from 'react';

View file

@ -35,6 +35,7 @@ import { StepModal } from '../shared/step_panel';
import { ApiKeyBanner } from './api_key_banner';
import { WindowsInstallStep } from '../shared/windows_install_step';
import { TroubleshootingLink } from '../shared/troubleshooting_link';
import { useFlowProgressTelemetry } from '../../../hooks/use_flow_progress_telemetry';
const defaultDatasetName = '';
@ -215,6 +216,8 @@ export function InstallElasticAgent() {
}
}, [progressSucceded, refetchProgress]);
useFlowProgressTelemetry(progressData?.progress, 'custom_logs');
const getCheckLogsStep = useCallback(() => {
const progress = progressData?.progress;
if (progress) {

View file

@ -35,6 +35,7 @@ import {
SystemIntegrationBanner,
SystemIntegrationBannerState,
} from './system_integration_banner';
import { useFlowProgressTelemetry } from '../../../hooks/use_flow_progress_telemetry';
export function InstallElasticAgent() {
const {
@ -174,6 +175,8 @@ export function InstallElasticAgent() {
}
}, [progressSucceded, refetchProgress]);
useFlowProgressTelemetry(progressData?.progress, 'system_logs');
const getCheckLogsStep = useCallback(() => {
const progress = progressData?.progress;
if (progress) {

View file

@ -19,6 +19,8 @@ import {
SingleDatasetLocatorParams,
SINGLE_DATASET_LOCATOR_ID,
} from '@kbn/deeplinks-observability/locators';
import { EaInstallProgressStepId } from '../../../../common/logs_flow_progress_step_id';
import { useFlowProgressTelemetry } from '../../../hooks/use_flow_progress_telemetry';
import { ObservabilityOnboardingPluginSetupDeps } from '../../../plugin';
import { useWizard } from '.';
import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher';
@ -28,7 +30,6 @@ import {
} from '../../shared/get_elastic_agent_setup_command';
import {
InstallElasticAgentSteps,
ProgressStepId,
EuiStepStatus,
} from '../../shared/install_elastic_agent_steps';
import {
@ -220,6 +221,8 @@ export function InstallElasticAgent() {
}
}, [progressSucceded, refetchProgress]);
useFlowProgressTelemetry(progressData?.progress, 'custom_logs');
const getCheckLogsStep = useCallback(() => {
const progress = progressData?.progress;
if (progress) {
@ -372,7 +375,7 @@ export function InstallElasticAgent() {
installProgressSteps={
(progressData?.progress ?? {}) as Partial<
Record<
ProgressStepId,
EaInstallProgressStepId,
{ status: EuiStepStatus; message?: string }
>
>

View file

@ -22,6 +22,8 @@ import {
import { i18n } from '@kbn/i18n';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { default as React, useCallback, useEffect, useState } from 'react';
import { EaInstallProgressStepId } from '../../../../common/logs_flow_progress_step_id';
import { useFlowProgressTelemetry } from '../../../hooks/use_flow_progress_telemetry';
import { useWizard } from '.';
import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher';
import { ObservabilityOnboardingPluginSetupDeps } from '../../../plugin';
@ -33,7 +35,6 @@ import {
import {
EuiStepStatus,
InstallElasticAgentSteps,
ProgressStepId,
} from '../../shared/install_elastic_agent_steps';
import {
StepPanel,
@ -186,6 +187,8 @@ export function InstallElasticAgent() {
}
}, [progressSucceded, refetchProgress]);
useFlowProgressTelemetry(progressData?.progress, 'system_logs');
const getCheckLogsStep = useCallback(() => {
const progress = progressData?.progress;
if (progress) {
@ -326,7 +329,7 @@ export function InstallElasticAgent() {
installProgressSteps={
(progressData?.progress ?? {}) as Partial<
Record<
ProgressStepId,
EaInstallProgressStepId,
{ status: EuiStepStatus; message?: string }
>
>

View file

@ -26,17 +26,11 @@ import { Buffer } from 'buffer';
import React, { ReactNode } from 'react';
import { intersection } from 'lodash';
import { FormattedMessage } from '@kbn/i18n-react';
import { EaInstallProgressStepId } from '../../../common/logs_flow_progress_step_id';
import { StepStatus } from './step_status';
export type EuiStepStatus = EuiStepsProps['steps'][number]['status'];
export type ProgressStepId =
| 'ea-download'
| 'ea-extract'
| 'ea-install'
| 'ea-status'
| 'ea-config';
interface Props<PlatformId extends string> {
installAgentPlatformOptions: Array<{
label: string;
@ -53,7 +47,7 @@ interface Props<PlatformId extends string> {
installAgentStatus: EuiStepStatus;
showInstallProgressSteps: boolean;
installProgressSteps: Partial<
Record<ProgressStepId, { status: EuiStepStatus; message?: string }>
Record<EaInstallProgressStepId, { status: EuiStepStatus; message?: string }>
>;
configureAgentStatus: EuiStepStatus;
configureAgentYaml: string;
@ -343,7 +337,7 @@ export function InstallElasticAgentSteps<PlatformId extends string>({
}
function getStep(
id: ProgressStepId,
id: EaInstallProgressStepId,
installProgressSteps: Props<string>['installProgressSteps'],
configPath: string
): { title: string; status: EuiStepStatus; message?: string } {
@ -374,7 +368,7 @@ function getStep(
const PROGRESS_STEP_TITLES: (
configPath: string
) => Record<
ProgressStepId,
EaInstallProgressStepId,
Record<'incompleteTitle' | 'loadingTitle' | 'completedTitle', string>
> = (configPath: string) => ({
'ea-download': {

View file

@ -0,0 +1,10 @@
/*
* 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 { createContext } from 'react';
export const ExperimentalOnboardingFeatureFlag = createContext(false);

View file

@ -0,0 +1,13 @@
/*
* 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 { useContext } from 'react';
import { ExperimentalOnboardingFeatureFlag } from '../context/experimental_onboarding_enabled';
export const useExperimentalOnboardingFlag = () => {
return useContext(ExperimentalOnboardingFeatureFlag);
};

View file

@ -0,0 +1,87 @@
/*
* 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 { renderHook } from '@testing-library/react-hooks';
import { useFlowProgressTelemetry } from './use_flow_progress_telemetry';
import { useKibana } from './use_kibana';
jest.mock('./use_kibana', () => {
return {
useKibana: jest.fn().mockReturnValue({
...jest.requireActual('./use_kibana'),
services: {
analytics: { reportEvent: jest.fn() },
},
}),
};
});
describe('useFlowProgressTelemetry', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('does not trigger an event if there is no progress', () => {
const render = renderHook(() => ({
analytics: useKibana().services.analytics,
flowProgress: useFlowProgressTelemetry(undefined, 'test-flow'),
}));
expect(render.result.current.analytics.reportEvent).not.toHaveBeenCalled();
});
it('triggers an event when there is a progress change', () => {
const render = renderHook(() => ({
analytics: useKibana().services.analytics,
flowProgress: useFlowProgressTelemetry(
{ 'ea-download': { status: 'complete' } },
'test-flow'
),
}));
expect(render.result.current.analytics.reportEvent).toHaveBeenCalledTimes(
1
);
expect(render.result.current.analytics.reportEvent).toHaveBeenCalledWith(
'observability_onboarding',
{
uses_legacy_onboarding_page: true,
flow: 'test-flow',
step: 'ea-download',
step_status: 'complete',
}
);
});
it('does not trigger an event for unsupported steps', () => {
const render = renderHook(() => ({
analytics: useKibana().services.analytics,
flowProgress: useFlowProgressTelemetry(
{ 'ea-extract': { status: 'complete' } },
'test-flow'
),
}));
expect(render.result.current.analytics.reportEvent).not.toHaveBeenCalled();
});
it('does not trigger an event if the status of a step has not changed', () => {
const render = renderHook(() => ({
analytics: useKibana().services.analytics,
flowProgress: useFlowProgressTelemetry(
{ 'ea-download': { status: 'complete' } },
'test-flow'
),
}));
render.rerender();
expect(render.result.current.analytics.reportEvent).toHaveBeenCalledTimes(
1
);
});
});

View file

@ -0,0 +1,73 @@
/*
* 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 { useEffect, useState } from 'react';
import { type LogsFlowProgressStepId } from '../../common/logs_flow_progress_step_id';
import { OBSERVABILITY_ONBOARDING_TELEMETRY_EVENT } from '../../common/telemetry_events';
import { type EuiStepStatus } from '../components/shared/install_elastic_agent_steps';
import { useExperimentalOnboardingFlag } from './use_experimental_onboarding_flag';
import { useKibana } from './use_kibana';
type StepsProgress = Partial<
Record<LogsFlowProgressStepId, { status: EuiStepStatus; message?: string }>
>;
const TRACKED_STEPS: LogsFlowProgressStepId[] = [
'ea-download',
'ea-status',
'logs-ingest',
];
const TRACKED_STATUSES: EuiStepStatus[] = ['danger', 'warning', 'complete'];
export function useFlowProgressTelemetry(
progress: StepsProgress | undefined,
flowId: string
) {
const {
services: { analytics },
} = useKibana();
const experimentalOnboardingFlowEnabled = useExperimentalOnboardingFlag();
const [previousReportedSteps] = useState<
Map<LogsFlowProgressStepId, EuiStepStatus>
>(new Map());
useEffect(() => {
if (!progress) {
return;
}
TRACKED_STEPS.forEach((stepId) => {
const step = progress[stepId];
if (
!step ||
!TRACKED_STATUSES.includes(step.status) ||
previousReportedSteps.get(stepId) === step.status
) {
return;
}
analytics.reportEvent(
OBSERVABILITY_ONBOARDING_TELEMETRY_EVENT.eventType,
{
uses_legacy_onboarding_page: !experimentalOnboardingFlowEnabled,
flow: flowId,
step: stepId,
step_status: step.status,
step_message: step.message,
}
);
previousReportedSteps.set(stepId, step.status);
});
}, [
analytics,
experimentalOnboardingFlowEnabled,
flowId,
progress,
previousReportedSteps,
]);
}

View file

@ -29,6 +29,7 @@ import { PLUGIN_ID } from '../common';
import { ObservabilityOnboardingLocatorDefinition } from './locators/onboarding_locator/locator_definition';
import { ObservabilityOnboardingPluginLocators } from './locators';
import { ConfigSchema } from '.';
import { OBSERVABILITY_ONBOARDING_TELEMETRY_EVENT } from '../common/telemetry_events';
export type ObservabilityOnboardingPluginSetup = void;
export type ObservabilityOnboardingPluginStart = void;
@ -164,6 +165,8 @@ export class ObservabilityOnboardingPlugin
),
};
core.analytics.registerEventType(OBSERVABILITY_ONBOARDING_TELEMETRY_EVENT);
return {
locators: this.locators,
};

View file

@ -35,7 +35,8 @@
"@kbn/deeplinks-observability",
"@kbn/fleet-plugin",
"@kbn/shared-ux-link-redirect-app",
"@kbn/cloud-experiments-plugin"
"@kbn/cloud-experiments-plugin",
"@kbn/analytics-client"
],
"exclude": ["target/**/*"]
}