From c4d63da03e4eb85a6de194de8cb6385a523cee05 Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Thu, 3 Mar 2022 10:57:38 +0200 Subject: [PATCH] APM execution context - app, page, entitiy id (#124996) * Client side execution app level context propagation * context$ + apm rum integration * invert the context parent \ child relationship (cc @mikhail) move more things to top level context * Pass down context to apm on server * types * eslint * parent <> child * docs + eslint + jest * execution context mock * eslint * jest * jest * server jest * check * jest * storybook * jest * report the current space * fix server side context container * Remove spaces for now * docssss * jest * lint * test * docs * revert file * doc * all context params are optional * clear on page change * lint * ts * skipped test again * testing fixes * oops * code review #1 * code review #2 * getAsLabels * maps inherit dashboard context * docs * ts * Give common context to all vis editors * fix test * ts \ es \ tests * labels * missing types * docsy docs * cr #3 * improve jest * Use editor name * Update src/plugins/visualizations/public/visualize_app/components/visualize_editor.tsx Co-authored-by: Marco Liberati * fix maps context * jest tests for maps * cr * docs * Update execution_context.test.ts * docs * lint Co-authored-by: Marco Liberati (cherry picked from commit d5416ed4aeed0bc19f4016aa5a4b63d911393f1b) # Conflicts: # src/plugins/discover/public/application/context/context_app.tsx # x-pack/plugins/lens/public/app_plugin/app.tsx --- ...-core-public.coresetup.executioncontext.md | 13 ++ .../kibana-plugin-core-public.coresetup.md | 1 + ...-core-public.corestart.executioncontext.md | 13 ++ .../kibana-plugin-core-public.corestart.md | 1 + ...core-public.executioncontextsetup.clear.md | 17 +++ ...e-public.executioncontextsetup.context_.md | 13 ++ ...n-core-public.executioncontextsetup.get.md | 17 +++ ...ublic.executioncontextsetup.getaslabels.md | 17 +++ ...lugin-core-public.executioncontextsetup.md | 30 ++++ ...n-core-public.executioncontextsetup.set.md | 24 ++++ ...executioncontextsetup.withglobalcontext.md | 24 ++++ ...lugin-core-public.executioncontextstart.md | 13 ++ ...ugin-core-public.kibanaexecutioncontext.md | 7 +- .../core/public/kibana-plugin-core-public.md | 2 + ...erver.executioncontextsetup.getaslabels.md | 15 ++ ...lugin-core-server.executioncontextsetup.md | 1 + ...ugin-core-server.kibanaexecutioncontext.md | 7 +- src/core/public/apm_system.test.ts | 4 + src/core/public/apm_system.ts | 12 ++ src/core/public/core_system.ts | 19 ++- .../execution_context_service.mock.ts | 35 +++++ .../execution_context_service.ts | 134 ++++++++++++++++++ src/core/public/execution_context/index.ts | 2 + src/core/public/http/fetch.test.ts | 40 +++++- src/core/public/http/fetch.ts | 8 +- src/core/public/http/http_service.test.ts | 10 +- src/core/public/http/http_service.ts | 7 +- src/core/public/index.ts | 11 +- src/core/public/mocks.ts | 3 + src/core/public/plugins/plugin_context.ts | 2 + .../public/plugins/plugins_service.test.ts | 3 + src/core/public/public.api.md | 28 +++- .../execution_context_container.ts | 5 +- .../execution_context_service.mock.ts | 2 + .../execution_context_service.ts | 23 +++ src/core/server/http/http_server.ts | 7 +- src/core/server/plugins/plugin_context.ts | 1 + src/core/server/server.api.md | 10 +- src/core/test_helpers/http_test_setup.ts | 4 +- src/core/types/execution_context.ts | 10 +- .../public/application/dashboard_app.tsx | 8 +- .../hooks/use_dashboard_app_state.ts | 4 - .../application/listing/dashboard_listing.tsx | 6 + .../search_interceptor.test.ts | 8 +- .../search_interceptor/search_interceptor.ts | 7 +- .../data/public/search/search_service.ts | 3 +- .../data/server/search/routes/bsearch.ts | 10 +- src/plugins/dev_tools/public/application.tsx | 16 ++- src/plugins/dev_tools/public/plugin.ts | 3 +- .../application/context/context_app.test.tsx | 3 + .../application/context/context_app.tsx | 9 +- .../application/doc/single_doc_route.tsx | 9 +- .../application/main/discover_main_route.tsx | 7 + .../main/utils/fetch_chart.test.ts | 6 +- .../application/main/utils/fetch_chart.ts | 4 - .../main/utils/fetch_documents.test.ts | 4 - .../application/main/utils/fetch_documents.ts | 4 - .../main/utils/fetch_total_hits.test.ts | 4 - .../main/utils/fetch_total_hits.ts | 4 - src/plugins/kibana_react/public/index.ts | 2 + .../public/use_execution_context/index.ts | 9 ++ .../use_execution_context.ts | 28 ++++ .../embeddable/visualize_embeddable.tsx | 16 +-- src/plugins/visualizations/public/plugin.ts | 2 + src/plugins/visualizations/public/services.ts | 4 + .../components/visualize_editor.tsx | 10 +- .../components/visualize_listing.tsx | 8 +- .../.storybook/context/execution_context.ts | 27 ++++ .../fleet/.storybook/context/index.tsx | 2 + .../__jest__/policy_table.test.tsx | 2 + x-pack/plugins/lens/public/app_plugin/app.tsx | 16 ++- .../lens/public/app_plugin/mounter.tsx | 1 + .../plugins/lens/public/app_plugin/types.ts | 2 + .../lens/public/mocks/services_mock.tsx | 1 + .../maps/common/execution_context.test.ts | 36 +++++ .../plugins/maps/common/execution_context.ts | 18 +-- .../es_geo_grid_source.test.ts | 15 +- x-pack/plugins/maps/public/kibana_services.ts | 1 + .../routes/list_page/maps_list_view.tsx | 7 + .../routes/map_page/map_app/map_app.tsx | 7 + x-pack/plugins/maps/public/util.test.js | 54 ++++++- x-pack/plugins/maps/public/util.ts | 30 +++- .../plugins/maps/server/mvt/get_grid_tile.ts | 5 +- x-pack/plugins/maps/server/mvt/get_tile.ts | 5 +- 84 files changed, 909 insertions(+), 113 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-core-public.coresetup.executioncontext.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.corestart.executioncontext.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.executioncontextsetup.clear.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.executioncontextsetup.context_.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.executioncontextsetup.get.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.executioncontextsetup.getaslabels.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.executioncontextsetup.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.executioncontextsetup.set.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.executioncontextsetup.withglobalcontext.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.executioncontextstart.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.getaslabels.md create mode 100644 src/core/public/execution_context/execution_context_service.mock.ts create mode 100644 src/core/public/execution_context/execution_context_service.ts create mode 100644 src/plugins/kibana_react/public/use_execution_context/index.ts create mode 100644 src/plugins/kibana_react/public/use_execution_context/use_execution_context.ts create mode 100644 x-pack/plugins/fleet/.storybook/context/execution_context.ts create mode 100644 x-pack/plugins/maps/common/execution_context.test.ts diff --git a/docs/development/core/public/kibana-plugin-core-public.coresetup.executioncontext.md b/docs/development/core/public/kibana-plugin-core-public.coresetup.executioncontext.md new file mode 100644 index 0000000000000..be5689ad7b080 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.coresetup.executioncontext.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [CoreSetup](./kibana-plugin-core-public.coresetup.md) > [executionContext](./kibana-plugin-core-public.coresetup.executioncontext.md) + +## CoreSetup.executionContext property + +[ExecutionContextSetup](./kibana-plugin-core-public.executioncontextsetup.md) + +Signature: + +```typescript +executionContext: ExecutionContextSetup; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.coresetup.md b/docs/development/core/public/kibana-plugin-core-public.coresetup.md index 9488b8a26b867..31793ec6f7a58 100644 --- a/docs/development/core/public/kibana-plugin-core-public.coresetup.md +++ b/docs/development/core/public/kibana-plugin-core-public.coresetup.md @@ -17,6 +17,7 @@ export interface CoreSetup + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [CoreStart](./kibana-plugin-core-public.corestart.md) > [executionContext](./kibana-plugin-core-public.corestart.executioncontext.md) + +## CoreStart.executionContext property + +[ExecutionContextStart](./kibana-plugin-core-public.executioncontextstart.md) + +Signature: + +```typescript +executionContext: ExecutionContextStart; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.corestart.md b/docs/development/core/public/kibana-plugin-core-public.corestart.md index ae67696e12501..edd80e1adb9f9 100644 --- a/docs/development/core/public/kibana-plugin-core-public.corestart.md +++ b/docs/development/core/public/kibana-plugin-core-public.corestart.md @@ -20,6 +20,7 @@ export interface CoreStart | [chrome](./kibana-plugin-core-public.corestart.chrome.md) | ChromeStart | [ChromeStart](./kibana-plugin-core-public.chromestart.md) | | [deprecations](./kibana-plugin-core-public.corestart.deprecations.md) | DeprecationsServiceStart | [DeprecationsServiceStart](./kibana-plugin-core-public.deprecationsservicestart.md) | | [docLinks](./kibana-plugin-core-public.corestart.doclinks.md) | DocLinksStart | [DocLinksStart](./kibana-plugin-core-public.doclinksstart.md) | +| [executionContext](./kibana-plugin-core-public.corestart.executioncontext.md) | ExecutionContextStart | [ExecutionContextStart](./kibana-plugin-core-public.executioncontextstart.md) | | [fatalErrors](./kibana-plugin-core-public.corestart.fatalerrors.md) | FatalErrorsStart | [FatalErrorsStart](./kibana-plugin-core-public.fatalerrorsstart.md) | | [http](./kibana-plugin-core-public.corestart.http.md) | HttpStart | [HttpStart](./kibana-plugin-core-public.httpstart.md) | | [i18n](./kibana-plugin-core-public.corestart.i18n.md) | I18nStart | [I18nStart](./kibana-plugin-core-public.i18nstart.md) | diff --git a/docs/development/core/public/kibana-plugin-core-public.executioncontextsetup.clear.md b/docs/development/core/public/kibana-plugin-core-public.executioncontextsetup.clear.md new file mode 100644 index 0000000000000..94936b94d0710 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.executioncontextsetup.clear.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ExecutionContextSetup](./kibana-plugin-core-public.executioncontextsetup.md) > [clear](./kibana-plugin-core-public.executioncontextsetup.clear.md) + +## ExecutionContextSetup.clear() method + +clears the context + +Signature: + +```typescript +clear(): void; +``` +Returns: + +void + diff --git a/docs/development/core/public/kibana-plugin-core-public.executioncontextsetup.context_.md b/docs/development/core/public/kibana-plugin-core-public.executioncontextsetup.context_.md new file mode 100644 index 0000000000000..d6c74db6d603e --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.executioncontextsetup.context_.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ExecutionContextSetup](./kibana-plugin-core-public.executioncontextsetup.md) > [context$](./kibana-plugin-core-public.executioncontextsetup.context_.md) + +## ExecutionContextSetup.context$ property + +The current context observable + +Signature: + +```typescript +context$: Observable; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.executioncontextsetup.get.md b/docs/development/core/public/kibana-plugin-core-public.executioncontextsetup.get.md new file mode 100644 index 0000000000000..65e9b1218649d --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.executioncontextsetup.get.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ExecutionContextSetup](./kibana-plugin-core-public.executioncontextsetup.md) > [get](./kibana-plugin-core-public.executioncontextsetup.get.md) + +## ExecutionContextSetup.get() method + +Get the current top level context + +Signature: + +```typescript +get(): KibanaExecutionContext; +``` +Returns: + +KibanaExecutionContext + diff --git a/docs/development/core/public/kibana-plugin-core-public.executioncontextsetup.getaslabels.md b/docs/development/core/public/kibana-plugin-core-public.executioncontextsetup.getaslabels.md new file mode 100644 index 0000000000000..0f0bda4e2913e --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.executioncontextsetup.getaslabels.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ExecutionContextSetup](./kibana-plugin-core-public.executioncontextsetup.md) > [getAsLabels](./kibana-plugin-core-public.executioncontextsetup.getaslabels.md) + +## ExecutionContextSetup.getAsLabels() method + +returns apm labels + +Signature: + +```typescript +getAsLabels(): Labels; +``` +Returns: + +Labels + diff --git a/docs/development/core/public/kibana-plugin-core-public.executioncontextsetup.md b/docs/development/core/public/kibana-plugin-core-public.executioncontextsetup.md new file mode 100644 index 0000000000000..01581d2e80a5c --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.executioncontextsetup.md @@ -0,0 +1,30 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ExecutionContextSetup](./kibana-plugin-core-public.executioncontextsetup.md) + +## ExecutionContextSetup interface + +Kibana execution context. Used to provide execution context to Elasticsearch, reporting, performance monitoring, etc. + +Signature: + +```typescript +export interface ExecutionContextSetup +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [context$](./kibana-plugin-core-public.executioncontextsetup.context_.md) | Observable<KibanaExecutionContext> | The current context observable | + +## Methods + +| Method | Description | +| --- | --- | +| [clear()](./kibana-plugin-core-public.executioncontextsetup.clear.md) | clears the context | +| [get()](./kibana-plugin-core-public.executioncontextsetup.get.md) | Get the current top level context | +| [getAsLabels()](./kibana-plugin-core-public.executioncontextsetup.getaslabels.md) | returns apm labels | +| [set(c$)](./kibana-plugin-core-public.executioncontextsetup.set.md) | Set the current top level context | +| [withGlobalContext(context)](./kibana-plugin-core-public.executioncontextsetup.withglobalcontext.md) | merges the current top level context with the specific event context | + diff --git a/docs/development/core/public/kibana-plugin-core-public.executioncontextsetup.set.md b/docs/development/core/public/kibana-plugin-core-public.executioncontextsetup.set.md new file mode 100644 index 0000000000000..e3dcea78c827a --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.executioncontextsetup.set.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ExecutionContextSetup](./kibana-plugin-core-public.executioncontextsetup.md) > [set](./kibana-plugin-core-public.executioncontextsetup.set.md) + +## ExecutionContextSetup.set() method + +Set the current top level context + +Signature: + +```typescript +set(c$: KibanaExecutionContext): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| c$ | KibanaExecutionContext | | + +Returns: + +void + diff --git a/docs/development/core/public/kibana-plugin-core-public.executioncontextsetup.withglobalcontext.md b/docs/development/core/public/kibana-plugin-core-public.executioncontextsetup.withglobalcontext.md new file mode 100644 index 0000000000000..574d0fd989750 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.executioncontextsetup.withglobalcontext.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ExecutionContextSetup](./kibana-plugin-core-public.executioncontextsetup.md) > [withGlobalContext](./kibana-plugin-core-public.executioncontextsetup.withglobalcontext.md) + +## ExecutionContextSetup.withGlobalContext() method + +merges the current top level context with the specific event context + +Signature: + +```typescript +withGlobalContext(context?: KibanaExecutionContext): KibanaExecutionContext; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| context | KibanaExecutionContext | | + +Returns: + +KibanaExecutionContext + diff --git a/docs/development/core/public/kibana-plugin-core-public.executioncontextstart.md b/docs/development/core/public/kibana-plugin-core-public.executioncontextstart.md new file mode 100644 index 0000000000000..0d210ba5bb1c4 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.executioncontextstart.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ExecutionContextStart](./kibana-plugin-core-public.executioncontextstart.md) + +## ExecutionContextStart type + +See [ExecutionContextSetup](./kibana-plugin-core-public.executioncontextsetup.md). + +Signature: + +```typescript +export declare type ExecutionContextStart = ExecutionContextSetup; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.md b/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.md index 6266639b63976..d8f8a77d84b2f 100644 --- a/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.md +++ b/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.md @@ -10,9 +10,10 @@ Represents a meta-information about a Kibana entity initiating a search request. ```typescript export declare type KibanaExecutionContext = { - readonly type: string; - readonly name: string; - readonly id: string; + readonly type?: string; + readonly name?: string; + readonly page?: string; + readonly id?: string; readonly description?: string; readonly url?: string; child?: KibanaExecutionContext; diff --git a/docs/development/core/public/kibana-plugin-core-public.md b/docs/development/core/public/kibana-plugin-core-public.md index b51f5ed833fd3..fca697144a872 100644 --- a/docs/development/core/public/kibana-plugin-core-public.md +++ b/docs/development/core/public/kibana-plugin-core-public.md @@ -62,6 +62,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [DeprecationsServiceStart](./kibana-plugin-core-public.deprecationsservicestart.md) | DeprecationsService provides methods to fetch domain deprecation details from the Kibana server. | | [DocLinksStart](./kibana-plugin-core-public.doclinksstart.md) | | | [ErrorToastOptions](./kibana-plugin-core-public.errortoastoptions.md) | Options available for [IToasts](./kibana-plugin-core-public.itoasts.md) error APIs. | +| [ExecutionContextSetup](./kibana-plugin-core-public.executioncontextsetup.md) | Kibana execution context. Used to provide execution context to Elasticsearch, reporting, performance monitoring, etc. | | [FatalErrorInfo](./kibana-plugin-core-public.fatalerrorinfo.md) | Represents the message and stack of a fatal Error | | [FatalErrorsSetup](./kibana-plugin-core-public.fatalerrorssetup.md) | FatalErrors stop the Kibana Public Core and displays a fatal error screen with details about the Kibana build and the error. | | [HttpFetchOptions](./kibana-plugin-core-public.httpfetchoptions.md) | All options that may be used with a [HttpHandler](./kibana-plugin-core-public.httphandler.md). | @@ -160,6 +161,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ChromeBreadcrumb](./kibana-plugin-core-public.chromebreadcrumb.md) | | | [ChromeHelpExtensionLinkBase](./kibana-plugin-core-public.chromehelpextensionlinkbase.md) | | | [ChromeHelpExtensionMenuLink](./kibana-plugin-core-public.chromehelpextensionmenulink.md) | | +| [ExecutionContextStart](./kibana-plugin-core-public.executioncontextstart.md) | See [ExecutionContextSetup](./kibana-plugin-core-public.executioncontextsetup.md). | | [FatalErrorsStart](./kibana-plugin-core-public.fatalerrorsstart.md) | FatalErrors stop the Kibana Public Core and displays a fatal error screen with details about the Kibana build and the error. | | [HttpStart](./kibana-plugin-core-public.httpstart.md) | See [HttpSetup](./kibana-plugin-core-public.httpsetup.md) | | [IToasts](./kibana-plugin-core-public.itoasts.md) | Methods for adding and removing global toast messages. See [ToastsApi](./kibana-plugin-core-public.toastsapi.md). | diff --git a/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.getaslabels.md b/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.getaslabels.md new file mode 100644 index 0000000000000..c8816a3deee4d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.getaslabels.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ExecutionContextSetup](./kibana-plugin-core-server.executioncontextsetup.md) > [getAsLabels](./kibana-plugin-core-server.executioncontextsetup.getaslabels.md) + +## ExecutionContextSetup.getAsLabels() method + +Signature: + +```typescript +getAsLabels(): apm.Labels; +``` +Returns: + +apm.Labels + diff --git a/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.md b/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.md index 24591648ad953..7fdc4d1ec1d57 100644 --- a/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.md @@ -15,5 +15,6 @@ export interface ExecutionContextSetup | Method | Description | | --- | --- | +| [getAsLabels()](./kibana-plugin-core-server.executioncontextsetup.getaslabels.md) | | | [withContext(context, fn)](./kibana-plugin-core-server.executioncontextsetup.withcontext.md) | Keeps track of execution context while the passed function is executed. Data are carried over all async operations spawned by the passed function. The nested calls stack the registered context on top of each other. | diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.md b/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.md index 0d65a3662da6f..792af8f693869 100644 --- a/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.md +++ b/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.md @@ -10,9 +10,10 @@ Represents a meta-information about a Kibana entity initiating a search request. ```typescript export declare type KibanaExecutionContext = { - readonly type: string; - readonly name: string; - readonly id: string; + readonly type?: string; + readonly name?: string; + readonly page?: string; + readonly id?: string; readonly description?: string; readonly url?: string; child?: KibanaExecutionContext; diff --git a/src/core/public/apm_system.test.ts b/src/core/public/apm_system.test.ts index 842d5de7e5afc..0a3a1dee63e57 100644 --- a/src/core/public/apm_system.test.ts +++ b/src/core/public/apm_system.test.ts @@ -13,6 +13,7 @@ import type { Transaction } from '@elastic/apm-rum'; import { ApmSystem } from './apm_system'; import { Subject } from 'rxjs'; import { InternalApplicationStart } from './application/types'; +import { executionContextServiceMock } from './execution_context/execution_context_service.mock'; const initMock = init as jest.Mocked; const apmMock = apm as DeeplyMockedKeys; @@ -96,6 +97,7 @@ describe('ApmSystem', () => { application: { currentAppId$, } as any as InternalApplicationStart, + executionContext: executionContextServiceMock.createInternalStartContract(), }); expect(mark).toHaveBeenCalledWith('apm-start'); @@ -118,6 +120,7 @@ describe('ApmSystem', () => { application: { currentAppId$, } as any as InternalApplicationStart, + executionContext: executionContextServiceMock.createInternalStartContract(), }); currentAppId$.next('myapp'); @@ -145,6 +148,7 @@ describe('ApmSystem', () => { application: { currentAppId$, } as any as InternalApplicationStart, + executionContext: executionContextServiceMock.createInternalStartContract(), }); currentAppId$.next('myapp'); diff --git a/src/core/public/apm_system.ts b/src/core/public/apm_system.ts index 2231f394381f0..4e116c0a0182d 100644 --- a/src/core/public/apm_system.ts +++ b/src/core/public/apm_system.ts @@ -10,6 +10,7 @@ import type { ApmBase, AgentConfigOptions, Transaction } from '@elastic/apm-rum' import { modifyUrl } from '@kbn/std'; import { CachedResourceObserver } from './apm_resource_counter'; import type { InternalApplicationStart } from './application'; +import { ExecutionContextStart } from './execution_context'; /** "GET protocol://hostname:port/pathname" */ const HTTP_REQUEST_TRANSACTION_NAME_REGEX = @@ -27,6 +28,7 @@ interface ApmConfig extends AgentConfigOptions { interface StartDeps { application: InternalApplicationStart; + executionContext: ExecutionContextStart; } export class ApmSystem { @@ -34,6 +36,7 @@ export class ApmSystem { private pageLoadTransaction?: Transaction; private resourceObserver: CachedResourceObserver; private apm?: ApmBase; + /** * `apmConfig` would be populated with relevant APM RUM agent * configuration if server is started with elastic.apm.* config. @@ -64,6 +67,15 @@ export class ApmSystem { this.markPageLoadStart(); + start.executionContext.context$.subscribe((c) => { + // We're using labels because we want the context to be indexed + // https://www.elastic.co/guide/en/apm/get-started/current/metadata.html + const apmContext = start.executionContext.getAsLabels(); + this.apm?.addLabels(apmContext); + }); + + // TODO: Start a new transaction every page change instead of every app change. + /** * Register listeners for navigation changes and capture them as * route-change transactions after Kibana app is bootstrapped diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index 3d3331d54792b..1aa01c13dd375 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -31,6 +31,7 @@ import { DeprecationsService } from './deprecations'; import { ThemeService } from './theme'; import { CoreApp } from './core_app'; import type { InternalApplicationSetup, InternalApplicationStart } from './application/types'; +import { ExecutionContextService } from './execution_context'; interface Params { rootDomElement: HTMLElement; @@ -87,6 +88,7 @@ export class CoreSystem { private readonly theme: ThemeService; private readonly rootDomElement: HTMLElement; private readonly coreContext: CoreContext; + private readonly executionContext: ExecutionContextService; private fatalErrorsSetup: FatalErrorsSetup | null = null; constructor(params: Params) { @@ -121,6 +123,7 @@ export class CoreSystem { this.application = new ApplicationService(); this.integrations = new IntegrationsService(); this.deprecations = new DeprecationsService(); + this.executionContext = new ExecutionContextService(); this.plugins = new PluginsService(this.coreContext, injectedMetadata.uiPlugins); this.coreApp = new CoreApp(this.coreContext); @@ -137,7 +140,13 @@ export class CoreSystem { }); await this.integrations.setup(); this.docLinks.setup(); - const http = this.http.setup({ injectedMetadata, fatalErrors: this.fatalErrorsSetup }); + + const executionContext = this.executionContext.setup(); + const http = this.http.setup({ + injectedMetadata, + fatalErrors: this.fatalErrorsSetup, + executionContext, + }); const uiSettings = this.uiSettings.setup({ http, injectedMetadata }); const notifications = this.notifications.setup({ uiSettings }); const theme = this.theme.setup({ injectedMetadata }); @@ -153,6 +162,7 @@ export class CoreSystem { notifications, theme, uiSettings, + executionContext, }; // Services that do not expose contracts at setup @@ -201,6 +211,11 @@ export class CoreSystem { targetDomElement: notificationsTargetDomElement, }); const application = await this.application.start({ http, theme, overlays }); + + const executionContext = this.executionContext.start({ + curApp$: application.currentAppId$, + }); + const chrome = await this.chrome.start({ application, docLinks, @@ -216,6 +231,7 @@ export class CoreSystem { application, chrome, docLinks, + executionContext, http, theme, savedObjects, @@ -248,6 +264,7 @@ export class CoreSystem { return { application, + executionContext, }; } catch (error) { if (this.fatalErrorsSetup) { diff --git a/src/core/public/execution_context/execution_context_service.mock.ts b/src/core/public/execution_context/execution_context_service.mock.ts new file mode 100644 index 0000000000000..3941aa333cfa2 --- /dev/null +++ b/src/core/public/execution_context/execution_context_service.mock.ts @@ -0,0 +1,35 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { PublicMethodsOf } from '@kbn/utility-types'; +import { BehaviorSubject } from 'rxjs'; + +import { ExecutionContextService, ExecutionContextSetup } from './execution_context_service'; + +const createContractMock = (): jest.Mocked => ({ + context$: new BehaviorSubject({}), + clear: jest.fn(), + set: jest.fn(), + get: jest.fn(), + getAsLabels: jest.fn(), + withGlobalContext: jest.fn(), +}); + +const createMock = (): jest.Mocked> => ({ + setup: jest.fn().mockReturnValue(createContractMock()), + start: jest.fn().mockReturnValue(createContractMock()), + stop: jest.fn(), +}); + +export const executionContextServiceMock = { + create: createMock, + createSetupContract: createContractMock, + createStartContract: createContractMock, + createInternalSetupContract: createContractMock, + createInternalStartContract: createContractMock, +}; diff --git a/src/core/public/execution_context/execution_context_service.ts b/src/core/public/execution_context/execution_context_service.ts new file mode 100644 index 0000000000000..bf13a7351f9b5 --- /dev/null +++ b/src/core/public/execution_context/execution_context_service.ts @@ -0,0 +1,134 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { isEqual, isUndefined, omitBy } from 'lodash'; +import { BehaviorSubject, Observable, Subscription } from 'rxjs'; +import { CoreService, KibanaExecutionContext } from '../../types'; + +// Should be exported from elastic/apm-rum +export type LabelValue = string | number | boolean; + +export interface Labels { + [key: string]: LabelValue; +} + +/** + * Kibana execution context. + * Used to provide execution context to Elasticsearch, reporting, performance monitoring, etc. + * @public + **/ +export interface ExecutionContextSetup { + /** + * The current context observable + **/ + context$: Observable; + /** + * Set the current top level context + **/ + set(c$: KibanaExecutionContext): void; + /** + * Get the current top level context + **/ + get(): KibanaExecutionContext; + /** + * clears the context + **/ + clear(): void; + /** + * returns apm labels + **/ + getAsLabels(): Labels; + /** + * merges the current top level context with the specific event context + **/ + withGlobalContext(context?: KibanaExecutionContext): KibanaExecutionContext; +} + +/** + * See {@link ExecutionContextSetup}. + * @public + */ +export type ExecutionContextStart = ExecutionContextSetup; + +export interface StartDeps { + curApp$: Observable; +} + +/** @internal */ +export class ExecutionContextService + implements CoreService +{ + private context$: BehaviorSubject = new BehaviorSubject({}); + private appId?: string; + private subscription: Subscription = new Subscription(); + private contract?: ExecutionContextSetup; + + public setup() { + this.contract = { + context$: this.context$.asObservable(), + clear: () => { + this.context$.next({}); + }, + set: (c: KibanaExecutionContext) => { + const newVal = { + ...this.context$.value, + ...c, + }; + if (!isEqual(newVal, this.context$.value)) { + this.context$.next(newVal); + } + }, + get: () => { + return this.mergeContext(); + }, + getAsLabels: () => { + return this.removeUndefined({ + name: this.appId, + id: this.context$.value?.id, + page: this.context$.value?.page, + }) as Labels; + }, + withGlobalContext: (context: KibanaExecutionContext) => { + return this.mergeContext(context); + }, + }; + + return this.contract; + } + + public start({ curApp$ }: StartDeps) { + const start = this.contract!; + + // Track app id changes and clear context on app change + this.subscription.add( + curApp$.subscribe((appId) => { + this.appId = appId; + start.clear(); + }) + ); + + return start; + } + + public stop() { + this.subscription.unsubscribe(); + } + + private removeUndefined(context: KibanaExecutionContext = {}) { + return omitBy(context, isUndefined); + } + + private mergeContext(context: KibanaExecutionContext = {}): KibanaExecutionContext { + return { + name: this.appId, + url: window.location.pathname, + ...this.context$.value, + ...context, + }; + } +} diff --git a/src/core/public/execution_context/index.ts b/src/core/public/execution_context/index.ts index b15a967ac714a..f160b0ecea67b 100644 --- a/src/core/public/execution_context/index.ts +++ b/src/core/public/execution_context/index.ts @@ -8,3 +8,5 @@ export type { KibanaExecutionContext } from '../../types'; export { ExecutionContextContainer } from './execution_context_container'; +export { ExecutionContextService } from './execution_context_service'; +export type { ExecutionContextSetup, ExecutionContextStart } from './execution_context_service'; diff --git a/src/core/public/http/fetch.test.ts b/src/core/public/http/fetch.test.ts index e897d69057e02..0691e2443c17f 100644 --- a/src/core/public/http/fetch.test.ts +++ b/src/core/public/http/fetch.test.ts @@ -15,6 +15,7 @@ import { first } from 'rxjs/operators'; import { Fetch } from './fetch'; import { BasePath } from './base_path'; import { HttpResponse, HttpFetchOptionsWithPath } from './types'; +import { executionContextServiceMock } from '../execution_context/execution_context_service.mock'; function delay(duration: number) { return new Promise((r) => setTimeout(r, duration)); @@ -23,9 +24,11 @@ function delay(duration: number) { const BASE_PATH = 'http://localhost/myBase'; describe('Fetch', () => { + const executionContextMock = executionContextServiceMock.createSetupContract(); const fetchInstance = new Fetch({ basePath: new BasePath(BASE_PATH), kibanaVersion: 'VERSION', + executionContext: executionContextMock, }); afterEach(() => { fetchMock.restore(); @@ -230,13 +233,15 @@ describe('Fetch', () => { it('should inject context headers if provided', async () => { fetchMock.get('*', {}); + const context = { + type: 'test-type', + name: 'test-name', + description: 'test-description', + id: '42', + }; + executionContextMock.withGlobalContext.mockReturnValue(context); await fetchInstance.fetch('/my/path', { - context: { - type: 'test-type', - name: 'test-name', - description: 'test-description', - id: '42', - }, + context, }); expect(fetchMock.lastOptions()!.headers).toMatchObject({ @@ -245,6 +250,29 @@ describe('Fetch', () => { }); }); + it('should include top level context context headers if provided', async () => { + fetchMock.get('*', {}); + + const context = { + type: 'test-type', + name: 'test-name', + description: 'test-description', + id: '42', + }; + executionContextMock.withGlobalContext.mockReturnValue({ + ...context, + name: 'banana', + }); + await fetchInstance.fetch('/my/path', { + context, + }); + + expect(fetchMock.lastOptions()!.headers).toMatchObject({ + 'x-kbn-context': + '%7B%22type%22%3A%22test-type%22%2C%22name%22%3A%22banana%22%2C%22description%22%3A%22test-description%22%2C%22id%22%3A%2242%22%7D', + }); + }); + it('should return response', async () => { fetchMock.get('*', { foo: 'bar' }); const json = await fetchInstance.fetch('/my/path'); diff --git a/src/core/public/http/fetch.ts b/src/core/public/http/fetch.ts index 4ee81f4b47aa0..9a333161e1b7a 100644 --- a/src/core/public/http/fetch.ts +++ b/src/core/public/http/fetch.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { omitBy } from 'lodash'; +import { isEmpty, omitBy } from 'lodash'; import { format } from 'url'; import { BehaviorSubject } from 'rxjs'; @@ -22,11 +22,12 @@ import { HttpFetchError } from './http_fetch_error'; import { HttpInterceptController } from './http_intercept_controller'; import { interceptRequest, interceptResponse } from './intercept'; import { HttpInterceptHaltError } from './http_intercept_halt_error'; -import { ExecutionContextContainer } from '../execution_context'; +import { ExecutionContextContainer, ExecutionContextSetup } from '../execution_context'; interface Params { basePath: IBasePath; kibanaVersion: string; + executionContext: ExecutionContextSetup; } const JSON_CONTENT = /^(application\/(json|x-javascript)|text\/(x-)?javascript|x-json)(;.*)?$/; @@ -107,6 +108,7 @@ export class Fetch { }; private createRequest(options: HttpFetchOptionsWithPath): Request { + const context = this.params.executionContext.withGlobalContext(options.context); // Merge and destructure options out that are not applicable to the Fetch API. const { query, @@ -125,7 +127,7 @@ export class Fetch { 'Content-Type': 'application/json', ...options.headers, 'kbn-version': this.params.kibanaVersion, - ...(options.context ? new ExecutionContextContainer(options.context).toHeader() : {}), + ...(!isEmpty(context) ? new ExecutionContextContainer(context).toHeader() : {}), }), }; diff --git a/src/core/public/http/http_service.test.ts b/src/core/public/http/http_service.test.ts index 2b41991904d97..698fa876433d4 100644 --- a/src/core/public/http/http_service.test.ts +++ b/src/core/public/http/http_service.test.ts @@ -14,6 +14,7 @@ import { fatalErrorsServiceMock } from '../fatal_errors/fatal_errors_service.moc import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock'; import { HttpService } from './http_service'; import { Observable } from 'rxjs'; +import { executionContextServiceMock } from '../execution_context/execution_context_service.mock'; describe('interceptors', () => { afterEach(() => fetchMock.restore()); @@ -22,9 +23,10 @@ describe('interceptors', () => { fetchMock.get('*', {}); const injectedMetadata = injectedMetadataServiceMock.createSetupContract(); const fatalErrors = fatalErrorsServiceMock.createSetupContract(); + const executionContext = executionContextServiceMock.createSetupContract(); const httpService = new HttpService(); - const setup = httpService.setup({ fatalErrors, injectedMetadata }); + const setup = httpService.setup({ fatalErrors, injectedMetadata, executionContext }); const setupInterceptor = jest.fn(); setup.intercept({ request: setupInterceptor }); @@ -47,7 +49,8 @@ describe('#setup()', () => { const injectedMetadata = injectedMetadataServiceMock.createSetupContract(); const fatalErrors = fatalErrorsServiceMock.createSetupContract(); const httpService = new HttpService(); - httpService.setup({ fatalErrors, injectedMetadata }); + const executionContext = executionContextServiceMock.createSetupContract(); + httpService.setup({ fatalErrors, injectedMetadata, executionContext }); const loadingServiceSetup = loadingServiceMock.setup.mock.results[0].value; // We don't verify that this Observable comes from Fetch#getLoadingCount$() to avoid complex mocking expect(loadingServiceSetup.addLoadingCountSource).toHaveBeenCalledWith(expect.any(Observable)); @@ -59,7 +62,8 @@ describe('#stop()', () => { const injectedMetadata = injectedMetadataServiceMock.createSetupContract(); const fatalErrors = fatalErrorsServiceMock.createSetupContract(); const httpService = new HttpService(); - httpService.setup({ fatalErrors, injectedMetadata }); + const executionContext = executionContextServiceMock.createSetupContract(); + httpService.setup({ fatalErrors, injectedMetadata, executionContext }); httpService.start(); httpService.stop(); expect(loadingServiceMock.stop).toHaveBeenCalled(); diff --git a/src/core/public/http/http_service.ts b/src/core/public/http/http_service.ts index a9719cfce67af..390130da4e07d 100644 --- a/src/core/public/http/http_service.ts +++ b/src/core/public/http/http_service.ts @@ -15,10 +15,12 @@ import { LoadingCountService } from './loading_count_service'; import { Fetch } from './fetch'; import { CoreService } from '../../types'; import { ExternalUrlService } from './external_url_service'; +import { ExecutionContextSetup } from '../execution_context'; interface HttpDeps { injectedMetadata: InjectedMetadataSetup; fatalErrors: FatalErrorsSetup; + executionContext: ExecutionContextSetup; } /** @internal */ @@ -27,14 +29,15 @@ export class HttpService implements CoreService { private readonly loadingCount = new LoadingCountService(); private service?: HttpSetup; - public setup({ injectedMetadata, fatalErrors }: HttpDeps): HttpSetup { + public setup({ injectedMetadata, fatalErrors, executionContext }: HttpDeps): HttpSetup { const kibanaVersion = injectedMetadata.getKibanaVersion(); const basePath = new BasePath( injectedMetadata.getBasePath(), injectedMetadata.getServerBasePath(), injectedMetadata.getPublicBaseUrl() ); - const fetchService = new Fetch({ basePath, kibanaVersion }); + + const fetchService = new Fetch({ basePath, kibanaVersion, executionContext }); const loadingCount = this.loadingCount.setup({ fatalErrors }); loadingCount.addLoadingCountSource(fetchService.getRequestCount$()); diff --git a/src/core/public/index.ts b/src/core/public/index.ts index ded7db9c6f892..50d8bf304ddf8 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -65,6 +65,7 @@ import { DocLinksStart } from './doc_links'; import { SavedObjectsStart } from './saved_objects'; import { DeprecationsServiceStart } from './deprecations'; import type { ThemeServiceSetup, ThemeServiceStart } from './theme'; +import { ExecutionContextSetup, ExecutionContextStart } from './execution_context'; export type { PackageInfo, @@ -194,7 +195,11 @@ export type { MountPoint, UnmountCallback, PublicUiSettingsParams } from './type export { URL_MAX_LENGTH } from './core_app'; -export type { KibanaExecutionContext } from './execution_context'; +export type { + KibanaExecutionContext, + ExecutionContextSetup, + ExecutionContextStart, +} from './execution_context'; /** * Core services exposed to the `Plugin` setup lifecycle @@ -221,6 +226,8 @@ export interface CoreSetup, any, any]>, []>(() => Promise.resolve([createCoreStartMock({ basePath }), pluginStartDeps, pluginStartContract]) @@ -76,6 +78,7 @@ function createCoreStartMock({ basePath = '' } = {}) { application: applicationServiceMock.createStartContract(), chrome: chromeServiceMock.createStartContract(), docLinks: docLinksServiceMock.createStartContract(), + executionContext: executionContextServiceMock.createStartContract(), http: httpServiceMock.createStartContract({ basePath }), i18n: i18nServiceMock.createStartContract(), notifications: notificationServiceMock.createStartContract(), diff --git a/src/core/public/plugins/plugin_context.ts b/src/core/public/plugins/plugin_context.ts index 345aea4b6cac8..8c085d3de2369 100644 --- a/src/core/public/plugins/plugin_context.ts +++ b/src/core/public/plugins/plugin_context.ts @@ -88,6 +88,7 @@ export function createPluginSetupContext< registerAppUpdater: (statusUpdater$) => deps.application.registerAppUpdater(statusUpdater$), }, fatalErrors: deps.fatalErrors, + executionContext: deps.executionContext, http: deps.http, notifications: deps.notifications, uiSettings: deps.uiSettings, @@ -129,6 +130,7 @@ export function createPluginStartContext< getUrlForApp: deps.application.getUrlForApp, }, docLinks: deps.docLinks, + executionContext: deps.executionContext, http: deps.http, chrome: omit(deps.chrome, 'getComponent'), i18n: deps.i18n, diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts index c4e3b7990ba32..40976424b7edd 100644 --- a/src/core/public/plugins/plugins_service.test.ts +++ b/src/core/public/plugins/plugins_service.test.ts @@ -36,6 +36,7 @@ import { docLinksServiceMock } from '../doc_links/doc_links_service.mock'; import { savedObjectsServiceMock } from '../saved_objects/saved_objects_service.mock'; import { deprecationsServiceMock } from '../deprecations/deprecations_service.mock'; import { themeServiceMock } from '../theme/theme_service.mock'; +import { executionContextServiceMock } from '../execution_context/execution_context_service.mock'; export let mockPluginInitializers: Map; @@ -85,6 +86,7 @@ describe('PluginsService', () => { mockSetupDeps = { application: applicationServiceMock.createInternalSetupContract(), fatalErrors: fatalErrorsServiceMock.createSetupContract(), + executionContext: executionContextServiceMock.createSetupContract(), http: httpServiceMock.createSetupContract(), injectedMetadata: injectedMetadataServiceMock.createStartContract(), notifications: notificationServiceMock.createSetupContract(), @@ -100,6 +102,7 @@ describe('PluginsService', () => { mockStartDeps = { application: applicationServiceMock.createInternalStartContract(), docLinks: docLinksServiceMock.createStartContract(), + executionContext: executionContextServiceMock.createStartContract(), http: httpServiceMock.createStartContract(), chrome: chromeServiceMock.createStartContract(), i18n: i18nServiceMock.createStartContract(), diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 1667ff369398f..a0b59700bbea2 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -399,6 +399,8 @@ export interface CoreSetup; @@ -427,6 +429,8 @@ export interface CoreStart { // (undocumented) docLinks: DocLinksStart; // (undocumented) + executionContext: ExecutionContextStart; + // (undocumented) fatalErrors: FatalErrorsStart; // (undocumented) http: HttpStart; @@ -459,6 +463,7 @@ export class CoreSystem { // (undocumented) start(): Promise<{ application: InternalApplicationStart; + executionContext: ExecutionContextSetup; } | undefined>; // (undocumented) stop(): void; @@ -509,6 +514,20 @@ export interface ErrorToastOptions extends ToastOptions { toastMessage?: string; } +// @public +export interface ExecutionContextSetup { + clear(): void; + context$: Observable; + get(): KibanaExecutionContext; + // Warning: (ae-forgotten-export) The symbol "Labels" needs to be exported by the entry point index.d.ts + getAsLabels(): Labels_2; + set(c$: KibanaExecutionContext): void; + withGlobalContext(context?: KibanaExecutionContext): KibanaExecutionContext; +} + +// @public +export type ExecutionContextStart = ExecutionContextSetup; + // @public export interface FatalErrorInfo { // (undocumented) @@ -749,9 +768,10 @@ export interface IUiSettingsClient { // @public export type KibanaExecutionContext = { - readonly type: string; - readonly name: string; - readonly id: string; + readonly type?: string; + readonly name?: string; + readonly page?: string; + readonly id?: string; readonly description?: string; readonly url?: string; child?: KibanaExecutionContext; @@ -1520,6 +1540,6 @@ export interface UserProvidedValues { // Warnings were encountered during analysis: // -// src/core/public/core_system.ts:173:21 - (ae-forgotten-export) The symbol "InternalApplicationStart" needs to be exported by the entry point index.d.ts +// src/core/public/core_system.ts:183:21 - (ae-forgotten-export) The symbol "InternalApplicationStart" needs to be exported by the entry point index.d.ts ``` diff --git a/src/core/server/execution_context/execution_context_container.ts b/src/core/server/execution_context/execution_context_container.ts index de895bcff5ecb..1528df6c23140 100644 --- a/src/core/server/execution_context/execution_context_container.ts +++ b/src/core/server/execution_context/execution_context_container.ts @@ -50,9 +50,10 @@ export interface IExecutionContextContainer { } function stringify(ctx: KibanaExecutionContext): string { - const stringifiedCtx = `${encodeURIComponent(ctx.type)}:${encodeURIComponent( + const encodeURIComponentIfNotEmpty = (val?: string) => encodeURIComponent(val || ''); + const stringifiedCtx = `${encodeURIComponentIfNotEmpty(ctx.type)}:${encodeURIComponentIfNotEmpty( ctx.name - )}:${encodeURIComponent(ctx.id!)}`; + )}:${encodeURIComponentIfNotEmpty(ctx.id)}`; return ctx.child ? `${stringifiedCtx};${stringify(ctx.child)}` : stringifiedCtx; } diff --git a/src/core/server/execution_context/execution_context_service.mock.ts b/src/core/server/execution_context/execution_context_service.mock.ts index 68aab7a5b84f8..85768eb423f26 100644 --- a/src/core/server/execution_context/execution_context_service.mock.ts +++ b/src/core/server/execution_context/execution_context_service.mock.ts @@ -26,6 +26,7 @@ const createExecutionContextMock = () => { get: jest.fn(), getParentContextFrom: jest.fn(), getAsHeader: jest.fn(), + getAsLabels: jest.fn(), }; mock.withContext.mockImplementation(withContextMock); return mock; @@ -38,6 +39,7 @@ const createInternalSetupContractMock = () => { const createSetupContractMock = () => { const mock: jest.Mocked = { withContext: jest.fn(), + getAsLabels: jest.fn(), }; mock.withContext.mockImplementation(withContextMock); return mock; diff --git a/src/core/server/execution_context/execution_context_service.ts b/src/core/server/execution_context/execution_context_service.ts index 6e2b809e23043..03ae2cb36c9ec 100644 --- a/src/core/server/execution_context/execution_context_service.ts +++ b/src/core/server/execution_context/execution_context_service.ts @@ -6,6 +6,8 @@ * Side Public License, v 1. */ import { AsyncLocalStorage } from 'async_hooks'; +import apm from 'elastic-apm-node'; +import { isUndefined, omitBy } from 'lodash'; import type { Subscription } from 'rxjs'; import type { CoreService, KibanaExecutionContext } from '../../types'; @@ -39,6 +41,10 @@ export interface IExecutionContext { * returns serialized representation to send as a header **/ getAsHeader(): string | undefined; + /** + * returns apm labels + **/ + getAsLabels(): apm.Labels; } /** @@ -61,6 +67,7 @@ export interface ExecutionContextSetup { * The nested calls stack the registered context on top of each other. **/ withContext(context: KibanaExecutionContext | undefined, fn: (...args: any[]) => R): R; + getAsLabels(): apm.Labels; } /** @@ -97,6 +104,7 @@ export class ExecutionContextService setRequestId: this.setRequestId.bind(this), get: this.get.bind(this), getAsHeader: this.getAsHeader.bind(this), + getAsLabels: this.getAsLabels.bind(this), }; } @@ -108,6 +116,7 @@ export class ExecutionContextService withContext: this.withContext.bind(this), get: this.get.bind(this), getAsHeader: this.getAsHeader.bind(this), + getAsLabels: this.getAsLabels.bind(this), }; } @@ -161,4 +170,18 @@ export class ExecutionContextService return `${requestId}${executionContextStr}`; } + + private getAsLabels() { + if (!this.enabled) return {}; + const executionContext = this.contextStore.getStore()?.toJSON(); + + return omitBy( + { + name: executionContext?.name, + id: executionContext?.id, + page: executionContext?.page, + }, + isUndefined + ); + } } diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index 4623b09b19e29..813f8e9784332 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -21,6 +21,7 @@ import agent from 'elastic-apm-node'; import type { Duration } from 'moment'; import { Observable } from 'rxjs'; import { take } from 'rxjs/operators'; +import apm from 'elastic-apm-node'; import { Logger, LoggerFactory } from '../logging'; import { HttpConfig } from './http_config'; import type { InternalExecutionContextSetup } from '../execution_context'; @@ -338,7 +339,11 @@ export class HttpServer { const requestId = getRequestId(request, config.requestId); const parentContext = executionContext?.getParentContextFrom(request.headers); - if (parentContext) executionContext?.set(parentContext); + + if (executionContext && parentContext) { + executionContext.set(parentContext); + apm.addLabels(executionContext.getAsLabels()); + } executionContext?.setRequestId(requestId); diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index 18abbe88c4913..983a12627b7f3 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -161,6 +161,7 @@ export function createPluginSetupContext( }, executionContext: { withContext: deps.executionContext.withContext, + getAsLabels: deps.executionContext.getAsLabels, }, http: { createCookieSessionStorageFactory: deps.http.createCookieSessionStorageFactory, diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 3f9f6ce99821e..798f3a5de7557 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -7,6 +7,7 @@ /// import { AddConfigDeprecation } from '@kbn/config'; +import apm from 'elastic-apm-node'; import Boom from '@hapi/boom'; import { ByteSizeValue } from '@kbn/config-schema'; import { CliArgs } from '@kbn/config'; @@ -1001,6 +1002,8 @@ export class EventLoopDelaysMonitor { // @public (undocumented) export interface ExecutionContextSetup { + // (undocumented) + getAsLabels(): apm.Labels; withContext(context: KibanaExecutionContext | undefined, fn: (...args: any[]) => R): R; } @@ -1326,9 +1329,10 @@ export interface IUiSettingsClient { // @public export type KibanaExecutionContext = { - readonly type: string; - readonly name: string; - readonly id: string; + readonly type?: string; + readonly name?: string; + readonly page?: string; + readonly id?: string; readonly description?: string; readonly url?: string; child?: KibanaExecutionContext; diff --git a/src/core/test_helpers/http_test_setup.ts b/src/core/test_helpers/http_test_setup.ts index 468034dffceb9..67b340898aab4 100644 --- a/src/core/test_helpers/http_test_setup.ts +++ b/src/core/test_helpers/http_test_setup.ts @@ -9,6 +9,7 @@ import { HttpService } from '../public/http'; import { fatalErrorsServiceMock } from '../public/fatal_errors/fatal_errors_service.mock'; import { injectedMetadataServiceMock } from '../public/injected_metadata/injected_metadata_service.mock'; +import { executionContextServiceMock } from '../public/execution_context/execution_context_service.mock'; export type SetupTap = ( injectedMetadata: ReturnType, @@ -28,7 +29,8 @@ export function setup(tap: SetupTap = defaultTap) { tap(injectedMetadata, fatalErrors); const httpService = new HttpService(); - const http = httpService.setup({ fatalErrors, injectedMetadata }); + const executionContext = executionContextServiceMock.createSetupContract(); + const http = httpService.setup({ fatalErrors, injectedMetadata, executionContext }); return { httpService, injectedMetadata, fatalErrors, http }; } diff --git a/src/core/types/execution_context.ts b/src/core/types/execution_context.ts index 1b985a73f410b..d790b8d855fd4 100644 --- a/src/core/types/execution_context.ts +++ b/src/core/types/execution_context.ts @@ -16,11 +16,13 @@ export type KibanaExecutionContext = { /** * Kibana application initated an operation. * */ - readonly type: string; // 'visualization' | 'actions' | 'server' | ..; - /** public name of a user-facing feature */ - readonly name: string; // 'TSVB' | 'Lens' | 'action_execution' | ..; + readonly type?: string; // 'visualization' | 'actions' | 'server' | ..; + /** public name of an application or a user-facing feature */ + readonly name?: string; // 'TSVB' | 'Lens' | 'action_execution' | ..; + /** a stand alone, logical unit such as an application page or tab */ + readonly page?: string; /** unique value to identify the source */ - readonly id: string; + readonly id?: string; /** human readable description. For example, a vis title, action name */ readonly description?: string; /** in browser - url to navigate to a current page, on server - endpoint path, for task: task SO url */ diff --git a/src/plugins/dashboard/public/application/dashboard_app.tsx b/src/plugins/dashboard/public/application/dashboard_app.tsx index 7aedbe9e11001..a32e6643a4e3a 100644 --- a/src/plugins/dashboard/public/application/dashboard_app.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app.tsx @@ -11,7 +11,7 @@ import React, { useEffect, useMemo } from 'react'; import { useDashboardSelector } from './state'; import { useDashboardAppState } from './hooks'; -import { useKibana } from '../../../kibana_react/public'; +import { useKibana, useExecutionContext } from '../../../kibana_react/public'; import { getDashboardBreadcrumb, getDashboardTitle, @@ -48,6 +48,12 @@ export function DashboardApp({ [core.notifications.toasts, history, uiSettings] ); + useExecutionContext(core.executionContext, { + type: 'application', + page: 'app', + id: savedDashboardId || 'new', + }); + const dashboardState = useDashboardSelector((state) => state.dashboardStateReducer); const dashboardAppState = useDashboardAppState({ history, diff --git a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts index 2ce1c87252d38..c0316e4e63441 100644 --- a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts +++ b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts @@ -220,11 +220,7 @@ export const useDashboardAppState = ({ savedDashboard, data, executionContext: { - type: 'application', - name: 'dashboard', - id: savedDashboard.id ?? 'unsaved_dashboard', description: savedDashboard.title, - url: history.location.pathname, }, }); if (canceled || !dashboardContainer) { diff --git a/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx b/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx index 5b53fc47e06a4..65374ad723f23 100644 --- a/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx +++ b/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx @@ -35,6 +35,7 @@ import { DashboardUnsavedListing } from './dashboard_unsaved_listing'; import { confirmCreateWithUnsaved, confirmDiscardUnsavedChanges } from './confirm_overlays'; import { getDashboardListItemLink } from './get_dashboard_list_item_link'; import { DASHBOARD_PANELS_UNSAVED_ID } from '../lib/dashboard_session_storage'; +import { useExecutionContext } from '../../../../kibana_react/public'; export interface DashboardListingProps { kbnUrlStateStorage: IKbnUrlStateStorage; @@ -67,6 +68,11 @@ export const DashboardListing = ({ dashboardSessionStorage.getDashboardIdsWithUnsavedChanges() ); + useExecutionContext(core.executionContext, { + type: 'application', + page: 'list', + }); + // Set breadcrumbs useEffect useEffect(() => { setBreadcrumbs([ diff --git a/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts b/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts index 968dd870489fe..f1e2e903cadde 100644 --- a/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts +++ b/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts @@ -119,6 +119,7 @@ describe('SearchInterceptor', () => { }), uiSettings: mockCoreSetup.uiSettings, http: mockCoreSetup.http, + executionContext: mockCoreSetup.executionContext, session: sessionService, theme: themeServiceMock.createSetupContract(), }); @@ -543,7 +544,12 @@ describe('SearchInterceptor', () => { .catch(() => {}); expect(fetchMock.mock.calls[0][0]).toEqual( expect.objectContaining({ - options: { sessionId, isStored: true, isRestore: true, strategy: 'ese' }, + options: { + sessionId, + isStored: true, + isRestore: true, + strategy: 'ese', + }, }) ); diff --git a/src/plugins/data/public/search/search_interceptor/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor/search_interceptor.ts index 7dc1ce6dee078..251e191d589e3 100644 --- a/src/plugins/data/public/search/search_interceptor/search_interceptor.ts +++ b/src/plugins/data/public/search/search_interceptor/search_interceptor.ts @@ -61,6 +61,7 @@ import { SearchAbortController } from './search_abort_controller'; export interface SearchInterceptorDeps { bfetch: BfetchPublicSetup; http: CoreSetup['http']; + executionContext: CoreSetup['executionContext']; uiSettings: CoreSetup['uiSettings']; startServices: Promise<[CoreStart, any, unknown]>; toasts: ToastsSetup; @@ -297,10 +298,14 @@ export class SearchInterceptor { } }) as Promise; } else { + const { executionContext, ...rest } = options || {}; return this.batchedFetch( { request, - options: this.getSerializableOptions(options), + options: this.getSerializableOptions({ + ...rest, + executionContext: this.deps.executionContext.withGlobalContext(executionContext), + }), }, abortSignal ); diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index 961599de713df..b21ad44c7bd6d 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -89,7 +89,7 @@ export class SearchService implements Plugin { constructor(private initializerContext: PluginInitializerContext) {} public setup( - { http, getStartServices, notifications, uiSettings, theme }: CoreSetup, + { http, getStartServices, notifications, uiSettings, executionContext, theme }: CoreSetup, { bfetch, expressions, usageCollection, nowProvider }: SearchServiceSetupDependencies ): ISearchSetup { this.usageCollector = createUsageCollector(getStartServices, usageCollection); @@ -108,6 +108,7 @@ export class SearchService implements Plugin { this.searchInterceptor = new SearchInterceptor({ bfetch, toasts: notifications.toasts, + executionContext, http, uiSettings, startServices: getStartServices(), diff --git a/src/plugins/data/server/search/routes/bsearch.ts b/src/plugins/data/server/search/routes/bsearch.ts index 314de4254851f..25b1bd0d7009d 100644 --- a/src/plugins/data/server/search/routes/bsearch.ts +++ b/src/plugins/data/server/search/routes/bsearch.ts @@ -9,6 +9,7 @@ import { catchError, first } from 'rxjs/operators'; import { BfetchServerSetup } from 'src/plugins/bfetch/server'; import type { ExecutionContextSetup } from 'src/core/server'; +import apm from 'elastic-apm-node'; import { IKibanaSearchRequest, IKibanaSearchResponse, @@ -33,9 +34,10 @@ export function registerBsearchRoute( */ onBatchItem: async ({ request: requestData, options }) => { const { executionContext, ...restOptions } = options || {}; + return executionContextService.withContext(executionContext, () => { + apm.addLabels(executionContextService.getAsLabels()); - return executionContextService.withContext(executionContext, () => - search + return search .search(requestData, restOptions) .pipe( first(), @@ -49,8 +51,8 @@ export function registerBsearchRoute( }; }) ) - .toPromise() - ); + .toPromise(); + }); }, }; }); diff --git a/src/plugins/dev_tools/public/application.tsx b/src/plugins/dev_tools/public/application.tsx index a3ec8fc0a9af2..bcfde68abd99c 100644 --- a/src/plugins/dev_tools/public/application.tsx +++ b/src/plugins/dev_tools/public/application.tsx @@ -15,8 +15,14 @@ import { I18nProvider } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import { euiThemeVars } from '@kbn/ui-theme'; -import { ApplicationStart, ChromeStart, ScopedHistory, CoreTheme } from 'src/core/public'; -import { KibanaThemeProvider } from '../../kibana_react/public'; +import type { + ApplicationStart, + ChromeStart, + ScopedHistory, + CoreTheme, + ExecutionContextStart, +} from 'src/core/public'; +import { KibanaThemeProvider, useExecutionContext } from '../../kibana_react/public'; import type { DocTitleService, BreadcrumbService } from './services'; import { DevToolApp } from './dev_tool'; @@ -24,6 +30,7 @@ import { DevToolApp } from './dev_tool'; export interface AppServices { docTitleService: DocTitleService; breadcrumbService: BreadcrumbService; + executionContext: ExecutionContextStart; } interface DevToolsWrapperProps { @@ -64,6 +71,11 @@ function DevToolsWrapper({ breadcrumbService.setBreadcrumbs(activeDevTool.title); }, [activeDevTool, docTitleService, breadcrumbService]); + useExecutionContext(appServices.executionContext, { + type: 'application', + page: activeDevTool.id, + }); + return (
diff --git a/src/plugins/dev_tools/public/plugin.ts b/src/plugins/dev_tools/public/plugin.ts index 1876bf278513e..ee729c8f4400c 100644 --- a/src/plugins/dev_tools/public/plugin.ts +++ b/src/plugins/dev_tools/public/plugin.ts @@ -61,7 +61,7 @@ export class DevToolsPlugin implements Plugin { element.classList.add('devAppWrapper'); const [core] = await getStartServices(); - const { application, chrome } = core; + const { application, chrome, executionContext } = core; this.docTitleService.setup(chrome.docTitle.change); this.breadcrumbService.setup(chrome.setBreadcrumbs); @@ -69,6 +69,7 @@ export class DevToolsPlugin implements Plugin { const appServices = { breadcrumbService: this.breadcrumbService, docTitleService: this.docTitleService, + executionContext, }; const { renderApp } = await import('./application'); diff --git a/src/plugins/discover/public/application/context/context_app.test.tsx b/src/plugins/discover/public/application/context/context_app.test.tsx index aa3428e52fa96..b66cbeeb23a8f 100644 --- a/src/plugins/discover/public/application/context/context_app.test.tsx +++ b/src/plugins/discover/public/application/context/context_app.test.tsx @@ -46,6 +46,9 @@ describe('ContextApp test', () => { toastNotifications: { addDanger: () => {} }, navigation: mockNavigationPlugin, core: { + executionContext: { + set: jest.fn(), + }, notifications: { toasts: [] }, theme: { theme$: themeServiceMock.createStartContract().theme$ }, }, diff --git a/src/plugins/discover/public/application/context/context_app.tsx b/src/plugins/discover/public/application/context/context_app.tsx index f93bc2b49fdd5..36ee03e17894d 100644 --- a/src/plugins/discover/public/application/context/context_app.tsx +++ b/src/plugins/discover/public/application/context/context_app.tsx @@ -26,6 +26,7 @@ import { ContextAppContent } from './context_app_content'; import { SurrDocType } from './services/context'; import { DocViewFilterFn } from '../../services/doc_views/doc_views_types'; import { useDiscoverServices } from '../../utils/use_discover_services'; +import { useExecutionContext } from '../../../../kibana_react/public'; const ContextAppContentMemoized = memo(ContextAppContent); @@ -36,11 +37,17 @@ export interface ContextAppProps { export const ContextApp = ({ indexPattern, anchorId }: ContextAppProps) => { const services = useDiscoverServices(); - const { uiSettings, capabilities, indexPatterns, navigation, filterManager } = services; + const { uiSettings, capabilities, indexPatterns, navigation, filterManager, core } = services; const isLegacy = useMemo(() => uiSettings.get(DOC_TABLE_LEGACY), [uiSettings]); const useNewFieldsApi = useMemo(() => !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE), [uiSettings]); + useExecutionContext(core.executionContext, { + type: 'application', + page: 'context', + id: indexPattern.id || '', + }); + /** * Context app state */ diff --git a/src/plugins/discover/public/application/doc/single_doc_route.tsx b/src/plugins/discover/public/application/doc/single_doc_route.tsx index d11c6bdca76a0..e2bdc6dc799e6 100644 --- a/src/plugins/discover/public/application/doc/single_doc_route.tsx +++ b/src/plugins/discover/public/application/doc/single_doc_route.tsx @@ -16,6 +16,7 @@ import { withQueryParams } from '../../utils/with_query_params'; import { useMainRouteBreadcrumb } from '../../utils/use_navigation_props'; import { Doc } from './components/doc'; import { useDiscoverServices } from '../../utils/use_discover_services'; +import { useExecutionContext } from '../../../../kibana_react/public'; export interface SingleDocRouteProps { /** @@ -31,11 +32,17 @@ export interface DocUrlParams { const SingleDoc = ({ id }: SingleDocRouteProps) => { const services = useDiscoverServices(); - const { chrome, timefilter } = services; + const { chrome, timefilter, core } = services; const { indexPatternId, index } = useParams(); const breadcrumb = useMainRouteBreadcrumb(); + useExecutionContext(core.executionContext, { + type: 'application', + page: 'single-doc', + id: indexPatternId, + }); + useEffect(() => { chrome.setBreadcrumbs([ ...getRootBreadcrumbs(breadcrumb), diff --git a/src/plugins/discover/public/application/main/discover_main_route.tsx b/src/plugins/discover/public/application/main/discover_main_route.tsx index d5950085b94c7..dcf229d36b1e0 100644 --- a/src/plugins/discover/public/application/main/discover_main_route.tsx +++ b/src/plugins/discover/public/application/main/discover_main_route.tsx @@ -24,6 +24,7 @@ import { LoadingIndicator } from '../../components/common/loading_indicator'; import { DiscoverError } from '../../components/common/error_alert'; import { useDiscoverServices } from '../../utils/use_discover_services'; import { getUrlTracker } from '../../kibana_services'; +import { useExecutionContext } from '../../../../kibana_react/public'; const DiscoverMainAppMemoized = memo(DiscoverMainApp); @@ -50,6 +51,12 @@ export function DiscoverMainRoute() { >([]); const { id } = useParams(); + useExecutionContext(core.executionContext, { + type: 'application', + page: 'app', + id: id || 'new', + }); + const navigateToOverview = useCallback(() => { core.application.navigateToApp('kibanaOverview', { path: '#' }); }, [core.application]); diff --git a/src/plugins/discover/public/application/main/utils/fetch_chart.test.ts b/src/plugins/discover/public/application/main/utils/fetch_chart.test.ts index 4c68eff54f579..b1f736fa4b224 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_chart.test.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_chart.test.ts @@ -117,7 +117,7 @@ describe('test fetchCharts', () => { }); }); - test('fetch$ is called with execution context containing savedSearch id', async () => { + test('fetch$ is called with request specific execution context', async () => { const fetch$Mock = jest.fn().mockReturnValue(of(requestResult)); savedSearchMockWithTimeField.searchSource.fetch$ = fetch$Mock; @@ -126,10 +126,6 @@ describe('test fetchCharts', () => { expect(fetch$Mock.mock.calls[0][0].executionContext).toMatchInlineSnapshot(` Object { "description": "fetch chart data and total hits", - "id": "the-saved-search-id-with-timefield", - "name": "discover", - "type": "application", - "url": "/", } `); }); diff --git a/src/plugins/discover/public/application/main/utils/fetch_chart.ts b/src/plugins/discover/public/application/main/utils/fetch_chart.ts index 00cb9c43caccf..1ea2594a89d97 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_chart.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_chart.ts @@ -40,11 +40,7 @@ export function fetchChart( const chartAggConfigs = updateSearchSource(searchSource, interval, data); const executionContext = { - type: 'application', - name: 'discover', description: 'fetch chart data and total hits', - url: window.location.pathname, - id: savedSearch.id ?? '', }; const fetch$ = searchSource diff --git a/src/plugins/discover/public/application/main/utils/fetch_documents.test.ts b/src/plugins/discover/public/application/main/utils/fetch_documents.test.ts index 000d3282c38b3..1e73f5de3a3f6 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_documents.test.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_documents.test.ts @@ -57,10 +57,6 @@ describe('test fetchDocuments', () => { expect(fetch$Mock.mock.calls[0][0].executionContext).toMatchInlineSnapshot(` Object { "description": "fetch total hits", - "id": "the-saved-search-id", - "name": "discover", - "type": "application", - "url": "/", } `); }); diff --git a/src/plugins/discover/public/application/main/utils/fetch_documents.ts b/src/plugins/discover/public/application/main/utils/fetch_documents.ts index dbf972265547e..8338839e8b0ac 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_documents.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_documents.ts @@ -32,11 +32,7 @@ export const fetchDocuments = ( } const executionContext = { - type: 'application', - name: 'discover', description: 'fetch documents', - url: window.location.pathname, - id: savedSearch.id ?? '', }; const fetch$ = searchSource diff --git a/src/plugins/discover/public/application/main/utils/fetch_total_hits.test.ts b/src/plugins/discover/public/application/main/utils/fetch_total_hits.test.ts index ba7b6a765aa2e..a5485c1a2e2e9 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_total_hits.test.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_total_hits.test.ts @@ -51,10 +51,6 @@ describe('test fetchTotalHits', () => { expect(fetch$Mock.mock.calls[0][0].executionContext).toMatchInlineSnapshot(` Object { "description": "fetch total hits", - "id": "the-saved-search-id", - "name": "discover", - "type": "application", - "url": "/", } `); }); diff --git a/src/plugins/discover/public/application/main/utils/fetch_total_hits.ts b/src/plugins/discover/public/application/main/utils/fetch_total_hits.ts index af2d55e23cf32..e696570f05cf0 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_total_hits.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_total_hits.ts @@ -30,11 +30,7 @@ export function fetchTotalHits( } const executionContext = { - type: 'application', - name: 'discover', description: 'fetch total hits', - url: window.location.pathname, - id: savedSearch.id ?? '', }; const fetch$ = searchSource diff --git a/src/plugins/kibana_react/public/index.ts b/src/plugins/kibana_react/public/index.ts index baed839d41123..94e8b505f1dd2 100644 --- a/src/plugins/kibana_react/public/index.ts +++ b/src/plugins/kibana_react/public/index.ts @@ -39,6 +39,8 @@ export { createReactOverlays } from './overlays'; export { useUiSetting, useUiSetting$ } from './ui_settings'; +export { useExecutionContext } from './use_execution_context'; + export type { TableListViewProps, TableListViewState } from './table_list_view'; export { TableListView } from './table_list_view'; diff --git a/src/plugins/kibana_react/public/use_execution_context/index.ts b/src/plugins/kibana_react/public/use_execution_context/index.ts new file mode 100644 index 0000000000000..f36d094eb86d4 --- /dev/null +++ b/src/plugins/kibana_react/public/use_execution_context/index.ts @@ -0,0 +1,9 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { useExecutionContext } from './use_execution_context'; diff --git a/src/plugins/kibana_react/public/use_execution_context/use_execution_context.ts b/src/plugins/kibana_react/public/use_execution_context/use_execution_context.ts new file mode 100644 index 0000000000000..e2c538056153c --- /dev/null +++ b/src/plugins/kibana_react/public/use_execution_context/use_execution_context.ts @@ -0,0 +1,28 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { KibanaExecutionContext, CoreStart } from 'kibana/public'; +import useDeepCompareEffect from 'react-use/lib/useDeepCompareEffect'; + +/** + * Set and clean up application level execution context + * @param executionContext + * @param context + */ +export function useExecutionContext( + executionContext: CoreStart['executionContext'], + context: KibanaExecutionContext +) { + useDeepCompareEffect(() => { + executionContext.set(context); + + return () => { + executionContext.clear(); + }; + }, [context]); +} diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx b/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx index 884a26a85e6d0..4dc7021ce4bb9 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx @@ -39,7 +39,7 @@ import { ExpressionAstExpression, } from '../../../../plugins/expressions/public'; import { Vis, SerializedVis } from '../vis'; -import { getExpressions, getTheme, getUiActions } from '../services'; +import { getExecutionContext, getExpressions, getTheme, getUiActions } from '../services'; import { VIS_EVENT_TO_TRIGGER } from './events'; import { VisualizeEmbeddableFactoryDeps } from './visualize_embeddable_factory'; import { getSavedVisualization } from '../utils/saved_visualize_utils'; @@ -391,20 +391,18 @@ export class VisualizeEmbeddable }; private async updateHandler() { - const parentContext = this.parent?.getInput().executionContext; + const parentContext = this.parent?.getInput().executionContext || getExecutionContext().get(); const child: KibanaExecutionContext = { type: 'visualization', name: this.vis.type.name, - id: this.vis.id ?? 'an_unsaved_vis', + id: this.vis.id ?? 'new', description: this.vis.title || this.input.title || this.vis.type.name, url: this.output.editUrl, }; - const context = parentContext - ? { - ...parentContext, - child, - } - : child; + const context = { + ...parentContext, + child, + }; const expressionParams: IExpressionLoaderParams = { searchContext: { diff --git a/src/plugins/visualizations/public/plugin.ts b/src/plugins/visualizations/public/plugin.ts index eae4f704b7c3c..c081511bcd608 100644 --- a/src/plugins/visualizations/public/plugin.ts +++ b/src/plugins/visualizations/public/plugin.ts @@ -36,6 +36,7 @@ import { setDocLinks, setSpaces, setTheme, + setExecutionContext, } from './services'; import { createVisEmbeddableFromObject, @@ -361,6 +362,7 @@ export class VisualizationsPlugin setTimeFilter(data.query.timefilter.timefilter); setAggs(data.search.aggs); setOverlays(core.overlays); + setExecutionContext(core.executionContext); setChrome(core.chrome); if (spaces) { diff --git a/src/plugins/visualizations/public/services.ts b/src/plugins/visualizations/public/services.ts index 37aea45fa3f58..8564c8225f1a7 100644 --- a/src/plugins/visualizations/public/services.ts +++ b/src/plugins/visualizations/public/services.ts @@ -16,6 +16,7 @@ import type { SavedObjectsStart, DocLinksStart, ThemeServiceStart, + ExecutionContextSetup, } from '../../../core/public'; import type { TypesStart } from './vis_types'; import { createGetterSetter } from '../../../plugins/kibana_utils/public'; @@ -65,4 +66,7 @@ export const [getOverlays, setOverlays] = createGetterSetter('Over export const [getChrome, setChrome] = createGetterSetter('Chrome'); +export const [getExecutionContext, setExecutionContext] = + createGetterSetter('ExecutionContext'); + export const [getSpaces, setSpaces] = createGetterSetter('Spaces', false); diff --git a/src/plugins/visualizations/public/visualize_app/components/visualize_editor.tsx b/src/plugins/visualizations/public/visualize_app/components/visualize_editor.tsx index 45241ec501084..c281b211768a1 100644 --- a/src/plugins/visualizations/public/visualize_app/components/visualize_editor.tsx +++ b/src/plugins/visualizations/public/visualize_app/components/visualize_editor.tsx @@ -11,7 +11,7 @@ import React, { useEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; import { EventEmitter } from 'events'; -import { useKibana } from '../../../../kibana_react/public'; +import { useExecutionContext, useKibana } from '../../../../kibana_react/public'; import { useChromeVisibility, useSavedVisInstance, @@ -41,6 +41,14 @@ export const VisualizeEditor = ({ onAppLeave }: VisualizeAppProps) => { originatingApp, visualizationIdFromUrl ); + + const editorName = savedVisInstance?.vis.type.title.toLowerCase().replace(' ', '_') || ''; + useExecutionContext(services.executionContext, { + type: 'application', + page: `editor${editorName ? `:${editorName}` : ''}`, + id: visualizationIdFromUrl || 'new', + }); + const { appState, hasUnappliedChanges } = useVisualizeAppState( services, eventEmitter, diff --git a/src/plugins/visualizations/public/visualize_app/components/visualize_listing.tsx b/src/plugins/visualizations/public/visualize_app/components/visualize_listing.tsx index cf219b1cda117..a180cf78feeb2 100644 --- a/src/plugins/visualizations/public/visualize_app/components/visualize_listing.tsx +++ b/src/plugins/visualizations/public/visualize_app/components/visualize_listing.tsx @@ -21,7 +21,7 @@ import { findListItems } from '../../utils/saved_visualize_utils'; import { showNewVisModal } from '../../wizard'; import { getTypes } from '../../services'; import { SavedObjectsFindOptionsReference } from '../../../../../core/public'; -import { useKibana, TableListView } from '../../../../kibana_react/public'; +import { useKibana, TableListView, useExecutionContext } from '../../../../kibana_react/public'; import { VISUALIZE_ENABLE_LABS_SETTING } from '../../../../visualizations/public'; import { VisualizeServices } from '../types'; import { VisualizeConstants } from '../../../common/constants'; @@ -31,6 +31,7 @@ export const VisualizeListing = () => { const { services: { application, + executionContext, chrome, history, toastNotifications, @@ -49,6 +50,11 @@ export const VisualizeListing = () => { const closeNewVisModal = useRef(() => {}); const listingLimit = savedObjectsPublic.settings.getListingLimit(); + useExecutionContext(executionContext, { + type: 'application', + page: 'list', + }); + useEffect(() => { if (pathname === '/new') { // In case the user navigated to the page via the /visualize/new URL we start the dialog immediately diff --git a/x-pack/plugins/fleet/.storybook/context/execution_context.ts b/x-pack/plugins/fleet/.storybook/context/execution_context.ts new file mode 100644 index 0000000000000..d3a15e200129b --- /dev/null +++ b/x-pack/plugins/fleet/.storybook/context/execution_context.ts @@ -0,0 +1,27 @@ +/* + * 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 { ExecutionContextSetup } from 'kibana/public'; +import { of } from 'rxjs'; + +export const getExecutionContext = () => { + const exec: ExecutionContextSetup = { + context$: of({}), + get: () => { + return {}; + }, + clear: () => {}, + set: (context: Record) => {}, + getAsLabels: () => { + return {}; + }, + withGlobalContext: () => { + return {}; + }, + }; + + return exec; +}; diff --git a/x-pack/plugins/fleet/.storybook/context/index.tsx b/x-pack/plugins/fleet/.storybook/context/index.tsx index eb19a1145ba75..fbcbd4fd3a081 100644 --- a/x-pack/plugins/fleet/.storybook/context/index.tsx +++ b/x-pack/plugins/fleet/.storybook/context/index.tsx @@ -31,6 +31,7 @@ import { stubbedStartServices } from './stubs'; import { getDocLinks } from './doc_links'; import { getCloud } from './cloud'; import { getShare } from './share'; +import { getExecutionContext } from './execution_context'; // TODO: clintandrewhall - this is not ideal, or complete. The root context of Fleet applications // requires full start contracts of its dependencies. As a result, we have to mock all of those contracts @@ -52,6 +53,7 @@ export const StorybookContext: React.FC<{ storyContext?: StoryContext }> = ({ () => ({ ...stubbedStartServices, application: getApplication(), + executionContext: getExecutionContext(), chrome: getChrome(), cloud: { ...getCloud({ isCloudEnabled }), diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/policy_table.test.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/policy_table.test.tsx index 1a45b2c6d93dc..1458d8adbdc12 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/policy_table.test.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/policy_table.test.tsx @@ -25,11 +25,13 @@ import { init as initHttp } from '../public/application/services/http'; import { init as initUiMetric } from '../public/application/services/ui_metric'; import { KibanaContextProvider } from '../public/shared_imports'; import { PolicyListContextProvider } from '../public/application/sections/policy_list/policy_list_context'; +import { executionContextServiceMock } from 'src/core/public/execution_context/execution_context_service.mock'; initHttp( new HttpService().setup({ injectedMetadata: injectedMetadataServiceMock.createSetupContract(), fatalErrors: fatalErrorsServiceMock.createSetupContract(), + executionContext: executionContextServiceMock.createSetupContract(), }) ); initUiMetric(usageCollectionPluginMock.createSetupContract()); diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 45517298b0432..3e51d11cffba3 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -14,7 +14,7 @@ import { createKbnUrlStateStorage, withNotifyOnErrors, } from '../../../../../src/plugins/kibana_utils/public'; -import { useKibana } from '../../../../../src/plugins/kibana_react/public'; +import { useExecutionContext, useKibana } from '../../../../../src/plugins/kibana_react/public'; import { OnSaveProps } from '../../../../../src/plugins/saved_objects/public'; import { syncQueryStateWithUrl } from '../../../../../src/plugins/data/public'; import { LensAppProps, LensAppServices } from './types'; @@ -69,6 +69,7 @@ export function App({ getOriginatingAppName, spaces, http, + executionContext, // Temporarily required until the 'by value' paradigm is default. dashboardFeatureFlag, } = lensAppServices; @@ -105,6 +106,7 @@ export function App({ const [indicateNoData, setIndicateNoData] = useState(false); const [isSaveModalVisible, setIsSaveModalVisible] = useState(false); const [lastKnownDoc, setLastKnownDoc] = useState(undefined); + const savedObjectId = (initialInput as LensByReferenceInput)?.savedObjectId; useEffect(() => { if (currentDoc) { @@ -116,6 +118,12 @@ export function App({ setIndicateNoData(true); }, [setIndicateNoData]); + useExecutionContext(executionContext, { + type: 'application', + id: savedObjectId || 'new', + page: 'editor', + }); + useEffect(() => { if (indicateNoData) { setIndicateNoData(false); @@ -126,11 +134,9 @@ export function App({ () => Boolean( // Temporarily required until the 'by value' paradigm is default. - dashboardFeatureFlag.allowByValueEmbeddables && - isLinkedToOriginatingApp && - !(initialInput as LensByReferenceInput)?.savedObjectId + dashboardFeatureFlag.allowByValueEmbeddables && isLinkedToOriginatingApp && !savedObjectId ), - [dashboardFeatureFlag.allowByValueEmbeddables, isLinkedToOriginatingApp, initialInput] + [dashboardFeatureFlag.allowByValueEmbeddables, isLinkedToOriginatingApp, savedObjectId] ); useEffect(() => { diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx index ff45c996cce98..c86367d02bf43 100644 --- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -76,6 +76,7 @@ export async function getLensServices( usageCollection, savedObjectsTagging, attributeService, + executionContext: coreStart.executionContext, http: coreStart.http, chrome: coreStart.chrome, overlays: coreStart.overlays, diff --git a/x-pack/plugins/lens/public/app_plugin/types.ts b/x-pack/plugins/lens/public/app_plugin/types.ts index a5cd8bfef71f3..1b33383dee2d4 100644 --- a/x-pack/plugins/lens/public/app_plugin/types.ts +++ b/x-pack/plugins/lens/public/app_plugin/types.ts @@ -12,6 +12,7 @@ import type { ApplicationStart, AppMountParameters, ChromeStart, + ExecutionContextStart, HttpStart, IUiSettingsClient, NotificationsStart, @@ -98,6 +99,7 @@ export interface HistoryLocationState { export interface LensAppServices { http: HttpStart; + executionContext: ExecutionContextStart; chrome: ChromeStart; overlays: OverlayStart; storage: IStorageWrapper; diff --git a/x-pack/plugins/lens/public/mocks/services_mock.tsx b/x-pack/plugins/lens/public/mocks/services_mock.tsx index 9fa6d61370a17..6681811744da4 100644 --- a/x-pack/plugins/lens/public/mocks/services_mock.tsx +++ b/x-pack/plugins/lens/public/mocks/services_mock.tsx @@ -112,6 +112,7 @@ export function makeDefaultServices( chrome: core.chrome, overlays: core.overlays, uiSettings: core.uiSettings, + executionContext: core.executionContext, navigation: navigationStartMock, notifications: core.notifications, attributeService: makeAttributeService(), diff --git a/x-pack/plugins/maps/common/execution_context.test.ts b/x-pack/plugins/maps/common/execution_context.test.ts new file mode 100644 index 0000000000000..25e3813a7e8f0 --- /dev/null +++ b/x-pack/plugins/maps/common/execution_context.test.ts @@ -0,0 +1,36 @@ +/* + * 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 { makeExecutionContext } from './execution_context'; + +describe('makeExecutionContext', () => { + test('returns basic fields if nothing is provided', () => { + const context = makeExecutionContext({}); + expect(context).toStrictEqual({ + name: 'maps', + type: 'application', + }); + }); + + test('merges in context', () => { + const context = makeExecutionContext({ id: '123' }); + expect(context).toStrictEqual({ + name: 'maps', + type: 'application', + id: '123', + }); + }); + + test('omits undefined values', () => { + const context = makeExecutionContext({ id: '123', description: undefined }); + expect(context).toStrictEqual({ + name: 'maps', + type: 'application', + id: '123', + }); + }); +}); diff --git a/x-pack/plugins/maps/common/execution_context.ts b/x-pack/plugins/maps/common/execution_context.ts index 23de29cfa8cd7..4a11eb5d89029 100644 --- a/x-pack/plugins/maps/common/execution_context.ts +++ b/x-pack/plugins/maps/common/execution_context.ts @@ -5,14 +5,16 @@ * 2.0. */ +import { isUndefined, omitBy } from 'lodash'; import { APP_ID } from './constants'; -export function makeExecutionContext(id: string, url: string, description?: string) { - return { - name: APP_ID, - type: 'application', - id, - description: description || '', - url, - }; +export function makeExecutionContext(context: { id?: string; url?: string; description?: string }) { + return omitBy( + { + name: APP_ID, + type: 'application', + ...context, + }, + isUndefined + ); } diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts index e2f9959b25d31..a26bd341613b2 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts @@ -5,8 +5,14 @@ * 2.0. */ +import { coreMock } from '../../../../../../../src/core/public/mocks'; import { MapExtent, VectorSourceRequestMeta } from '../../../../common/descriptor_types'; -import { getHttp, getIndexPatternService, getSearchService } from '../../../kibana_services'; +import { + getExecutionContext, + getHttp, + getIndexPatternService, + getSearchService, +} from '../../../kibana_services'; import { ESGeoGridSource } from './es_geo_grid_source'; import { ES_GEO_FIELD_TYPE, @@ -129,6 +135,13 @@ describe('ESGeoGridSource', () => { }, }, }); + + const coreStartMock = coreMock.createStart(); + coreStartMock.executionContext.get.mockReturnValue({ + name: 'some-app', + }); + // @ts-expect-error + getExecutionContext.mockReturnValue(coreStartMock.executionContext); }); afterEach(() => { diff --git a/x-pack/plugins/maps/public/kibana_services.ts b/x-pack/plugins/maps/public/kibana_services.ts index 027981de32295..d56329ae102b4 100644 --- a/x-pack/plugins/maps/public/kibana_services.ts +++ b/x-pack/plugins/maps/public/kibana_services.ts @@ -33,6 +33,7 @@ export const getUiSettings = () => coreStart.uiSettings; export const getIsDarkMode = () => getUiSettings().get('theme:darkMode', false); export const getIndexPatternSelectComponent = () => pluginsStart.data.ui.IndexPatternSelect; export const getHttp = () => coreStart.http; +export const getExecutionContext = () => coreStart.executionContext; export const getTimeFilter = () => pluginsStart.data.query.timefilter.timefilter; export const getToasts = () => coreStart.notifications.toasts; export const getSavedObjectsClient = () => coreStart.savedObjects.client; diff --git a/x-pack/plugins/maps/public/routes/list_page/maps_list_view.tsx b/x-pack/plugins/maps/public/routes/list_page/maps_list_view.tsx index 571cba64a06c4..dab284b0b71e4 100644 --- a/x-pack/plugins/maps/public/routes/list_page/maps_list_view.tsx +++ b/x-pack/plugins/maps/public/routes/list_page/maps_list_view.tsx @@ -17,6 +17,7 @@ import { getMapsCapabilities, getToasts, getCoreChrome, + getExecutionContext, getNavigateToApp, getSavedObjectsClient, getSavedObjectsTagging, @@ -121,6 +122,12 @@ async function deleteMaps(items: object[]) { } export function MapsListView() { + getExecutionContext().set({ + type: 'application', + page: 'list', + id: '', + }); + const isReadOnly = !getMapsCapabilities().save; getCoreChrome().docTitle.change(getAppTitle()); diff --git a/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx b/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx index 6a2881a5dd274..437e06ac618ee 100644 --- a/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx +++ b/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx @@ -16,6 +16,7 @@ import { type Filter, FilterStateStore } from '@kbn/es-query'; import type { Query, TimeRange, IndexPattern } from 'src/plugins/data/common'; import { getData, + getExecutionContext, getCoreChrome, getMapsCapabilities, getNavigation, @@ -115,6 +116,12 @@ export class MapApp extends React.Component { componentDidMount() { this._isMounted = true; + getExecutionContext().set({ + type: 'application', + page: 'editor', + id: this.props.savedMap.getSavedObjectId() || 'new', + }); + this._autoRefreshSubscription = getTimeFilter() .getAutoRefreshFetch$() .pipe( diff --git a/x-pack/plugins/maps/public/util.test.js b/x-pack/plugins/maps/public/util.test.js index d8861063fc637..7fc88578b378a 100644 --- a/x-pack/plugins/maps/public/util.test.js +++ b/x-pack/plugins/maps/public/util.test.js @@ -5,7 +5,7 @@ * 2.0. */ -import { getGlyphUrl } from './util'; +import { getGlyphUrl, makePublicExecutionContext } from './util'; const MOCK_EMS_SETTINGS = { isEMSEnabled: () => true, @@ -62,3 +62,55 @@ describe('getGlyphUrl', () => { }); }); }); + +describe('makePublicExecutionContext', () => { + let injectedContext = {}; + beforeAll(() => { + require('./kibana_services').getExecutionContext = () => ({ + get: () => injectedContext, + }); + }); + + test('creates basic context when no top level context is provided', () => { + const context = makePublicExecutionContext('test'); + expect(context).toStrictEqual({ + description: 'test', + name: 'maps', + type: 'application', + url: '/', + }); + }); + + test('merges with top level context if its from the same app', () => { + injectedContext = { + name: 'maps', + id: '1234', + }; + const context = makePublicExecutionContext('test'); + expect(context).toStrictEqual({ + description: 'test', + name: 'maps', + type: 'application', + url: '/', + id: '1234', + }); + }); + + test('nests inside top level context if its from a different app', () => { + injectedContext = { + name: 'other-app', + id: '1234', + }; + const context = makePublicExecutionContext('test'); + expect(context).toStrictEqual({ + name: 'other-app', + id: '1234', + child: { + description: 'test', + type: 'application', + name: 'maps', + url: '/', + }, + }); + }); +}); diff --git a/x-pack/plugins/maps/public/util.ts b/x-pack/plugins/maps/public/util.ts index 4adb8b35bfcea..66244ea5f6768 100644 --- a/x-pack/plugins/maps/public/util.ts +++ b/x-pack/plugins/maps/public/util.ts @@ -8,7 +8,13 @@ import { EMSClient, FileLayer, TMSService } from '@elastic/ems-client'; import type { KibanaExecutionContext } from 'kibana/public'; import { FONTS_API_PATH } from '../common/constants'; -import { getHttp, getTilemap, getEMSSettings, getMapsEmsStart } from './kibana_services'; +import { + getHttp, + getTilemap, + getEMSSettings, + getMapsEmsStart, + getExecutionContext, +} from './kibana_services'; import { getLicenseId } from './licensed_features'; import { makeExecutionContext } from '../common/execution_context'; @@ -67,9 +73,21 @@ export function isRetina(): boolean { return window.devicePixelRatio === 2; } -export function makePublicExecutionContext( - id: string, - description?: string -): KibanaExecutionContext { - return makeExecutionContext(id, window.location.pathname, description); +export function makePublicExecutionContext(description: string): KibanaExecutionContext { + const topLevelContext = getExecutionContext().get(); + const context = makeExecutionContext({ + url: window.location.pathname, + description, + }); + + // Distinguish between running in maps app vs. embedded + return topLevelContext.name !== undefined && topLevelContext.name !== context.name + ? { + ...topLevelContext, + child: context, + } + : { + ...topLevelContext, + ...context, + }; } diff --git a/x-pack/plugins/maps/server/mvt/get_grid_tile.ts b/x-pack/plugins/maps/server/mvt/get_grid_tile.ts index 2eb55056692d6..5c1c1cea9b50c 100644 --- a/x-pack/plugins/maps/server/mvt/get_grid_tile.ts +++ b/x-pack/plugins/maps/server/mvt/get_grid_tile.ts @@ -56,7 +56,10 @@ export async function getEsGridTile({ }; const tile = await core.executionContext.withContext( - makeExecutionContext('mvt:get_grid_tile', url), + makeExecutionContext({ + description: 'mvt:get_grid_tile', + url, + }), async () => { return await context.core.elasticsearch.client.asCurrentUser.transport.request( { diff --git a/x-pack/plugins/maps/server/mvt/get_tile.ts b/x-pack/plugins/maps/server/mvt/get_tile.ts index 50b21433ebf2c..c79b127d70874 100644 --- a/x-pack/plugins/maps/server/mvt/get_tile.ts +++ b/x-pack/plugins/maps/server/mvt/get_tile.ts @@ -57,7 +57,10 @@ export async function getEsTile({ }; const tile = await core.executionContext.withContext( - makeExecutionContext('mvt:get_tile', url), + makeExecutionContext({ + description: 'mvt:get_tile', + url, + }), async () => { return await context.core.elasticsearch.client.asCurrentUser.transport.request( {