Skip to content

Commit

Permalink
[ObsUx] Add feedback form to apm (elastic#173758)
Browse files Browse the repository at this point in the history
Closes elastic#172506

## Summary

This PR adds a button to all APM pages navigating to a feedback form.
The feedback button component exists in infra so in this PR the
component is changed to meet the new requirements and moved to
`observability_shared`. Now the feedback button component supports also
prefilling the sanitized path (`entry.1876422621`). The requirements
picked for the sanitized path are described in the issue - it should be
for example `/app/apm/{page_name}` if the user is on the page with the
exact path matching `/app/apm/{page_name}` and `/app/apm/{page_name}*`
for all subpages (for example `/app/apm/{page_name}/something/else`) -
different test scenarios can be found in the unit test:
[get_path_for_feedback.test.ts](https://github.com/elastic/kibana/compare/main...jennypavlova:kibana:172506-obsux-add-feedback-form-to-apm?expand=1#diff-6396807e61353509c44fa38488dfb741549e60f25126024b92596f6b7ac933b8)

## Testing
- Go to APM
- All APM pages should have a yellow button with the text: `Tell us what
you think!`
- Hover on the button to see the prefilled properties and check if every
property is prefilled
<img width="1920" alt="image"
src="https://github.com/elastic/kibana/assets/14139027/b57adf04-4e7b-42bb-827f-db554dc4a842">

- Click on the form and check if the form is prefilled - The questions
that should be prefilled in APM
   - Where is your Elastic cluster deployed? (same as infra)
      - By default, the pre-selected option will be on-prem
- To test the case with the cloud option preselected add
`xpack.cloud.id: 'elastic_kibana_dev'` to your `kibana.dev.yaml`
- To test the serverless option run Kibana in serverless mode
(observability config)
   - What version of Elastic are you using? (same as infra)
   - Where in the UI are you?
- After checking the APM go to the infra pages and check the feedback
button/form




https://github.com/elastic/kibana/assets/14139027/52cc886f-95a9-4099-b047-93fc64036572
  • Loading branch information
jennypavlova authored and delanni committed Jan 11, 2024
1 parent be6cccd commit ec4fcf0
Show file tree
Hide file tree
Showing 23 changed files with 356 additions and 84 deletions.
4 changes: 4 additions & 0 deletions x-pack/plugins/apm/public/application/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { createCallApmApi } from '../services/rest/create_call_apm_api';
import { setHelpExtension } from '../set_help_extension';
import { setReadonlyBadge } from '../update_badge';
import { ApmAppRoot } from '../components/routing/app_root';
import type { KibanaEnvContext } from '../context/kibana_environment_context/kibana_environment_context';

/**
* This module is rendered asynchronously in the Kibana platform.
Expand All @@ -32,6 +33,7 @@ export const renderApp = ({
pluginsStart,
observabilityRuleTypeRegistry,
apmServices,
kibanaEnvironment,
}: {
coreStart: CoreStart;
pluginsSetup: ApmPluginSetupDeps;
Expand All @@ -40,6 +42,7 @@ export const renderApp = ({
pluginsStart: ApmPluginStartDeps;
observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry;
apmServices: ApmServices;
kibanaEnvironment: KibanaEnvContext;
}) => {
const { element, theme$ } = appMountParameters;
const apmPluginContextValue = {
Expand All @@ -58,6 +61,7 @@ export const renderApp = ({
uiActions: pluginsStart.uiActions,
observabilityAIAssistant: pluginsStart.observabilityAIAssistant,
share: pluginsSetup.share,
kibanaEnvironment,
};

// render APM feedback link in global help menu
Expand Down
87 changes: 47 additions & 40 deletions x-pack/plugins/apm/public/components/routing/app_root/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { RouteRenderer, RouterProvider } from '@kbn/typed-react-router-config';
import { euiDarkVars, euiLightVars } from '@kbn/ui-theme';
import React from 'react';
import { DefaultTheme, ThemeProvider } from 'styled-components';
import { useKibanaEnvironmentContextProvider } from '../../../context/kibana_environment_context/use_kibana_environment_context';
import { AnomalyDetectionJobsContextProvider } from '../../../context/anomaly_detection_jobs/anomaly_detection_jobs_context';
import {
ApmPluginContext,
Expand Down Expand Up @@ -54,7 +55,9 @@ export function ApmAppRoot({
pluginsStart: ApmPluginStartDeps;
apmServices: ApmServices;
}) {
const { appMountParameters, core } = apmPluginContextValue;
const { appMountParameters, kibanaEnvironment, core } = apmPluginContextValue;
const KibanaEnvironmentContextProvider =
useKibanaEnvironmentContextProvider(kibanaEnvironment);
const { history } = appMountParameters;
const i18nCore = core.i18n;

Expand All @@ -73,46 +76,50 @@ export function ApmAppRoot({
<KibanaContextProvider
services={{ ...core, ...pluginsStart, storage, ...apmServices }}
>
<i18nCore.Context>
<TimeRangeIdContextProvider>
<RouterProvider history={history} router={apmRouter as any}>
<ApmErrorBoundary>
<RedirectDependenciesToDependenciesInventory>
<RedirectWithDefaultEnvironment>
<RedirectWithDefaultDateRange>
<RedirectWithOffset>
<TrackPageview>
<UpdateExecutionContextOnRouteChange>
<BreadcrumbsContextProvider>
<UrlParamsProvider>
<LicenseProvider>
<AnomalyDetectionJobsContextProvider>
<InspectorContextProvider>
<ApmThemeProvider>
<MountApmHeaderActionMenu />
<KibanaEnvironmentContextProvider
kibanaEnvironment={kibanaEnvironment}
>
<i18nCore.Context>
<TimeRangeIdContextProvider>
<RouterProvider history={history} router={apmRouter as any}>
<ApmErrorBoundary>
<RedirectDependenciesToDependenciesInventory>
<RedirectWithDefaultEnvironment>
<RedirectWithDefaultDateRange>
<RedirectWithOffset>
<TrackPageview>
<UpdateExecutionContextOnRouteChange>
<BreadcrumbsContextProvider>
<UrlParamsProvider>
<LicenseProvider>
<AnomalyDetectionJobsContextProvider>
<InspectorContextProvider>
<ApmThemeProvider>
<MountApmHeaderActionMenu />

<Route
component={
ScrollToTopOnPathChange
}
/>
<RouteRenderer />
</ApmThemeProvider>
</InspectorContextProvider>
</AnomalyDetectionJobsContextProvider>
</LicenseProvider>
</UrlParamsProvider>
</BreadcrumbsContextProvider>
</UpdateExecutionContextOnRouteChange>
</TrackPageview>
</RedirectWithOffset>
</RedirectWithDefaultDateRange>
</RedirectWithDefaultEnvironment>
</RedirectDependenciesToDependenciesInventory>
</ApmErrorBoundary>
</RouterProvider>
</TimeRangeIdContextProvider>
</i18nCore.Context>
<Route
component={
ScrollToTopOnPathChange
}
/>
<RouteRenderer />
</ApmThemeProvider>
</InspectorContextProvider>
</AnomalyDetectionJobsContextProvider>
</LicenseProvider>
</UrlParamsProvider>
</BreadcrumbsContextProvider>
</UpdateExecutionContextOnRouteChange>
</TrackPageview>
</RedirectWithOffset>
</RedirectWithDefaultDateRange>
</RedirectWithDefaultEnvironment>
</RedirectDependenciesToDependenciesInventory>
</ApmErrorBoundary>
</RouterProvider>
</TimeRangeIdContextProvider>
</i18nCore.Context>
</KibanaEnvironmentContextProvider>
</KibanaContextProvider>
</ApmPluginContext.Provider>
</RedirectAppLinks>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ import { EuiFlexGroup, EuiFlexItem, EuiPageHeaderProps } from '@elastic/eui';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { ObservabilityPageTemplateProps } from '@kbn/observability-shared-plugin/public';
import type { KibanaPageTemplateProps } from '@kbn/shared-ux-page-kibana-template';
import React from 'react';
import React, { useContext } from 'react';
import { useLocation } from 'react-router-dom';
import { FeatureFeedbackButton } from '@kbn/observability-shared-plugin/public';
import { KibanaEnvironmentContext } from '../../../context/kibana_environment_context/kibana_environment_context';
import { getPathForFeedback } from '../../../utils/get_path_for_feedback';
import { EnvironmentsContextProvider } from '../../../context/environments_context/environments_context';
import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher';
import { ApmPluginStartDeps } from '../../../plugin';
Expand All @@ -22,6 +25,7 @@ import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_

// Paths that must skip the no data screen
const bypassNoDataScreenPaths = ['/settings', '/diagnostics'];
const APM_FEEDBACK_LINK = 'https://ela.st/services-feedback';

/*
* This template contains:
Expand Down Expand Up @@ -56,7 +60,9 @@ export function ApmMainTemplate({
const location = useLocation();

const { services } = useKibana<ApmPluginStartDeps>();
const kibanaEnvironment = useContext(KibanaEnvironmentContext);
const { http, docLinks, observabilityShared, application } = services;
const { kibanaVersion, isCloudEnv, isServerlessEnv } = kibanaEnvironment;
const basePath = http?.basePath.get();
const { config } = useApmPluginContext();

Expand All @@ -66,7 +72,7 @@ export function ApmMainTemplate({
return callApmApi('GET /internal/apm/has_data');
}, []);

// create static data view on inital load
// create static data view on initial load
useFetcher(
(callApmApi) => {
const canCreateDataView =
Expand Down Expand Up @@ -111,11 +117,26 @@ export function ApmMainTemplate({
...(showServiceGroupSaveButton ? [<ServiceGroupSaveButton />] : []),
];

const sanitizedPath = getPathForFeedback(window.location.pathname);
const pageHeaderTitle = (
<EuiFlexGroup justifyContent="spaceBetween" wrap={true}>
{pageHeader?.pageTitle ?? pageTitle}
<EuiFlexItem grow={false}>
{environmentFilter && <ApmEnvironmentFilter />}
<EuiFlexGroup justifyContent="center">
<EuiFlexItem grow={false}>
<FeatureFeedbackButton
data-test-subj="infraApmFeedbackLink"
formUrl={APM_FEEDBACK_LINK}
kibanaVersion={kibanaVersion}
isCloudEnv={isCloudEnv}
isServerlessEnv={isServerlessEnv}
sanitizedPath={sanitizedPath}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
{environmentFilter && <ApmEnvironmentFilter />}
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type { ObservabilityAIAssistantPluginStart } from '@kbn/observability-ai-
import { SharePluginSetup } from '@kbn/share-plugin/public';
import type { ApmPluginSetupDeps } from '../../plugin';
import type { ConfigSchema } from '../..';
import type { KibanaEnvContext } from '../kibana_environment_context/kibana_environment_context';

export interface ApmPluginContextValue {
appMountParameters: AppMountParameters;
Expand All @@ -34,6 +35,7 @@ export interface ApmPluginContextValue {
uiActions: UiActionsStart;
observabilityAIAssistant: ObservabilityAIAssistantPluginStart;
share: SharePluginSetup;
kibanaEnvironment: KibanaEnvContext;
}

export const ApmPluginContext = createContext({} as ApmPluginContextValue);
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* 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 interface KibanaEnvContext {
kibanaVersion?: string;
isCloudEnv?: boolean;
isServerlessEnv?: boolean;
}

export const KibanaEnvironmentContext = createContext<KibanaEnvContext>({});
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* 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 React, { useMemo, createElement } from 'react';
import {
KibanaEnvironmentContext,
type KibanaEnvContext,
} from './kibana_environment_context';

export const useKibanaEnvironmentContextProvider = ({
kibanaVersion,
isCloudEnv,
isServerlessEnv,
}: KibanaEnvContext) => {
const value = useMemo(
() => ({
kibanaVersion,
isCloudEnv,
isServerlessEnv,
}),
[kibanaVersion, isCloudEnv, isServerlessEnv]
);

const Provider: React.FC<{ kibanaEnvironment?: KibanaEnvContext }> = ({
kibanaEnvironment = {},
children,
}) => {
const newProvider = createElement(KibanaEnvironmentContext.Provider, {
value: { ...kibanaEnvironment, ...value },
children,
});

return newProvider;
};

return Provider;
};
19 changes: 17 additions & 2 deletions x-pack/plugins/apm/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ import { DashboardStart } from '@kbn/dashboard-plugin/public';
import type { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
import { from } from 'rxjs';
import { map } from 'rxjs/operators';
import type { CloudSetup } from '@kbn/cloud-plugin/public';
import type { ConfigSchema } from '.';
import { registerApmRuleTypes } from './components/alerting/rule_types/register_apm_rule_types';
import {
Expand Down Expand Up @@ -106,6 +107,7 @@ export interface ApmPluginSetupDeps {
share: SharePluginSetup;
uiActions: UiActionsSetup;
profiling?: ProfilingPluginSetup;
cloud?: CloudSetup;
}

export interface ApmServices {
Expand Down Expand Up @@ -190,11 +192,16 @@ const apmTutorialTitle = i18n.translate(

export class ApmPlugin implements Plugin<ApmPluginSetup, ApmPluginStart> {
private telemetry: TelemetryService;
private kibanaVersion: string;
private isServerlessEnv: boolean;
constructor(
private readonly initializerContext: PluginInitializerContext<ConfigSchema>
) {
this.initializerContext = initializerContext;
this.telemetry = new TelemetryService();
this.kibanaVersion = initializerContext.env.packageInfo.version;
this.isServerlessEnv =
initializerContext.env.packageInfo.buildFlavor === 'serverless';
}

public setup(core: CoreSetup, plugins: ApmPluginSetupDeps) {
Expand Down Expand Up @@ -396,17 +403,25 @@ export class ApmPlugin implements Plugin<ApmPluginSetup, ApmPluginStart> {
{ id: 'tutorial', title: apmTutorialTitle, path: '/tutorial' },
],

async mount(appMountParameters: AppMountParameters<unknown>) {
mount: async (appMountParameters: AppMountParameters<unknown>) => {
// Load application bundle and Get start services
const [{ renderApp }, [coreStart, pluginsStart]] = await Promise.all([
import('./application'),
core.getStartServices(),
]);
const isCloudEnv = !!pluginSetupDeps.cloud?.isCloudEnabled;
const isServerlessEnv =
pluginSetupDeps.cloud?.isServerlessEnabled || this.isServerlessEnv;
return renderApp({
coreStart,
pluginsSetup: pluginSetupDeps,
pluginsSetup: pluginSetupDeps as ApmPluginSetupDeps,
appMountParameters,
config,
kibanaEnvironment: {
isCloudEnv,
isServerlessEnv,
kibanaVersion: this.kibanaVersion,
},
pluginsStart: pluginsStart as ApmPluginStartDeps,
observabilityRuleTypeRegistry,
apmServices: {
Expand Down
Loading

0 comments on commit ec4fcf0

Please sign in to comment.