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 <dej611@users.noreply.github.com>

* fix maps context

* jest tests for maps

* cr

* docs

* Update execution_context.test.ts

* docs

* lint

Co-authored-by: Marco Liberati <dej611@users.noreply.github.com>
This commit is contained in:
Liza Katz 2022-03-03 10:57:38 +02:00 committed by GitHub
parent 8e7a7925f2
commit d5416ed4ae
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
84 changed files with 909 additions and 113 deletions

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [CoreSetup](./kibana-plugin-core-public.coresetup.md) &gt; [executionContext](./kibana-plugin-core-public.coresetup.executioncontext.md)
## CoreSetup.executionContext property
[ExecutionContextSetup](./kibana-plugin-core-public.executioncontextsetup.md)
<b>Signature:</b>
```typescript
executionContext: ExecutionContextSetup;
```

View file

@ -17,6 +17,7 @@ export interface CoreSetup<TPluginsStart extends object = object, TStart = unkno
| Property | Type | Description |
| --- | --- | --- |
| [application](./kibana-plugin-core-public.coresetup.application.md) | ApplicationSetup | [ApplicationSetup](./kibana-plugin-core-public.applicationsetup.md) |
| [executionContext](./kibana-plugin-core-public.coresetup.executioncontext.md) | ExecutionContextSetup | [ExecutionContextSetup](./kibana-plugin-core-public.executioncontextsetup.md) |
| [fatalErrors](./kibana-plugin-core-public.coresetup.fatalerrors.md) | FatalErrorsSetup | [FatalErrorsSetup](./kibana-plugin-core-public.fatalerrorssetup.md) |
| [getStartServices](./kibana-plugin-core-public.coresetup.getstartservices.md) | StartServicesAccessor&lt;TPluginsStart, TStart&gt; | [StartServicesAccessor](./kibana-plugin-core-public.startservicesaccessor.md) |
| [http](./kibana-plugin-core-public.coresetup.http.md) | HttpSetup | [HttpSetup](./kibana-plugin-core-public.httpsetup.md) |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [CoreStart](./kibana-plugin-core-public.corestart.md) &gt; [executionContext](./kibana-plugin-core-public.corestart.executioncontext.md)
## CoreStart.executionContext property
[ExecutionContextStart](./kibana-plugin-core-public.executioncontextstart.md)
<b>Signature:</b>
```typescript
executionContext: ExecutionContextStart;
```

View file

@ -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) |

View file

@ -0,0 +1,17 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [ExecutionContextSetup](./kibana-plugin-core-public.executioncontextsetup.md) &gt; [clear](./kibana-plugin-core-public.executioncontextsetup.clear.md)
## ExecutionContextSetup.clear() method
clears the context
<b>Signature:</b>
```typescript
clear(): void;
```
<b>Returns:</b>
void

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [ExecutionContextSetup](./kibana-plugin-core-public.executioncontextsetup.md) &gt; [context$](./kibana-plugin-core-public.executioncontextsetup.context_.md)
## ExecutionContextSetup.context$ property
The current context observable
<b>Signature:</b>
```typescript
context$: Observable<KibanaExecutionContext>;
```

View file

@ -0,0 +1,17 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [ExecutionContextSetup](./kibana-plugin-core-public.executioncontextsetup.md) &gt; [get](./kibana-plugin-core-public.executioncontextsetup.get.md)
## ExecutionContextSetup.get() method
Get the current top level context
<b>Signature:</b>
```typescript
get(): KibanaExecutionContext;
```
<b>Returns:</b>
KibanaExecutionContext

View file

@ -0,0 +1,17 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [ExecutionContextSetup](./kibana-plugin-core-public.executioncontextsetup.md) &gt; [getAsLabels](./kibana-plugin-core-public.executioncontextsetup.getaslabels.md)
## ExecutionContextSetup.getAsLabels() method
returns apm labels
<b>Signature:</b>
```typescript
getAsLabels(): Labels;
```
<b>Returns:</b>
Labels

View file

@ -0,0 +1,30 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [ExecutionContextSetup](./kibana-plugin-core-public.executioncontextsetup.md)
## ExecutionContextSetup interface
Kibana execution context. Used to provide execution context to Elasticsearch, reporting, performance monitoring, etc.
<b>Signature:</b>
```typescript
export interface ExecutionContextSetup
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [context$](./kibana-plugin-core-public.executioncontextsetup.context_.md) | Observable&lt;KibanaExecutionContext&gt; | 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 |

View file

@ -0,0 +1,24 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [ExecutionContextSetup](./kibana-plugin-core-public.executioncontextsetup.md) &gt; [set](./kibana-plugin-core-public.executioncontextsetup.set.md)
## ExecutionContextSetup.set() method
Set the current top level context
<b>Signature:</b>
```typescript
set(c$: KibanaExecutionContext): void;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| c$ | KibanaExecutionContext | |
<b>Returns:</b>
void

View file

@ -0,0 +1,24 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [ExecutionContextSetup](./kibana-plugin-core-public.executioncontextsetup.md) &gt; [withGlobalContext](./kibana-plugin-core-public.executioncontextsetup.withglobalcontext.md)
## ExecutionContextSetup.withGlobalContext() method
merges the current top level context with the specific event context
<b>Signature:</b>
```typescript
withGlobalContext(context?: KibanaExecutionContext): KibanaExecutionContext;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| context | KibanaExecutionContext | |
<b>Returns:</b>
KibanaExecutionContext

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [ExecutionContextStart](./kibana-plugin-core-public.executioncontextstart.md)
## ExecutionContextStart type
See [ExecutionContextSetup](./kibana-plugin-core-public.executioncontextsetup.md)<!-- -->.
<b>Signature:</b>
```typescript
export declare type ExecutionContextStart = ExecutionContextSetup;
```

View file

@ -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;

View file

@ -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 <code>message</code> and <code>stack</code> 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)<!-- -->. |

View file

@ -0,0 +1,15 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [ExecutionContextSetup](./kibana-plugin-core-server.executioncontextsetup.md) &gt; [getAsLabels](./kibana-plugin-core-server.executioncontextsetup.getaslabels.md)
## ExecutionContextSetup.getAsLabels() method
<b>Signature:</b>
```typescript
getAsLabels(): apm.Labels;
```
<b>Returns:</b>
apm.Labels

View file

@ -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. |

View file

@ -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;

View file

@ -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<typeof init>;
const apmMock = apm as DeeplyMockedKeys<typeof apm>;
@ -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');

View file

@ -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

View file

@ -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) {

View file

@ -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<ExecutionContextSetup> => ({
context$: new BehaviorSubject({}),
clear: jest.fn(),
set: jest.fn(),
get: jest.fn(),
getAsLabels: jest.fn(),
withGlobalContext: jest.fn(),
});
const createMock = (): jest.Mocked<PublicMethodsOf<ExecutionContextService>> => ({
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,
};

View file

@ -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<KibanaExecutionContext>;
/**
* 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<string | undefined>;
}
/** @internal */
export class ExecutionContextService
implements CoreService<ExecutionContextSetup, ExecutionContextStart>
{
private context$: BehaviorSubject<KibanaExecutionContext> = 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,
};
}
}

View file

@ -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';

View file

@ -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<T>(duration: number) {
return new Promise<T>((r) => setTimeout(r, duration));
@ -23,9 +24,11 @@ function delay<T>(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');

View file

@ -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() : {}),
}),
};

View file

@ -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();

View file

@ -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<HttpSetup, HttpStart> {
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$());

View file

@ -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<TPluginsStart extends object = object, TStart = unkno
notifications: NotificationsSetup;
/** {@link IUiSettingsClient} */
uiSettings: IUiSettingsClient;
/** {@link ExecutionContextSetup} */
executionContext: ExecutionContextSetup;
/**
* exposed temporarily until https://github.com/elastic/kibana/issues/41990 done
* use *only* to retrieve config values. There is no way to set injected values
@ -264,6 +271,8 @@ export interface CoreStart {
chrome: ChromeStart;
/** {@link DocLinksStart} */
docLinks: DocLinksStart;
/** {@link ExecutionContextStart} */
executionContext: ExecutionContextStart;
/** {@link HttpStart} */
http: HttpStart;
/** {@link SavedObjectsStart} */

View file

@ -29,6 +29,7 @@ import { themeServiceMock } from './theme/theme_service.mock';
export { chromeServiceMock } from './chrome/chrome_service.mock';
export { docLinksServiceMock } from './doc_links/doc_links_service.mock';
import { executionContextServiceMock } from './execution_context/execution_context_service.mock';
export { fatalErrorsServiceMock } from './fatal_errors/fatal_errors_service.mock';
export { httpServiceMock } from './http/http_service.mock';
export { i18nServiceMock } from './i18n/i18n_service.mock';
@ -54,6 +55,7 @@ function createCoreSetupMock({
const mock = {
application: applicationServiceMock.createSetupContract(),
docLinks: docLinksServiceMock.createSetupContract(),
executionContext: executionContextServiceMock.createSetupContract(),
fatalErrors: fatalErrorsServiceMock.createSetupContract(),
getStartServices: jest.fn<Promise<[ReturnType<typeof createCoreStartMock>, 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(),

View file

@ -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,

View file

@ -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<PluginName, MockedPluginInitializer>;
@ -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(),

View file

@ -401,6 +401,8 @@ export interface CoreSetup<TPluginsStart extends object = object, TStart = unkno
// (undocumented)
application: ApplicationSetup;
// (undocumented)
executionContext: ExecutionContextSetup;
// (undocumented)
fatalErrors: FatalErrorsSetup;
// (undocumented)
getStartServices: StartServicesAccessor<TPluginsStart, TStart>;
@ -429,6 +431,8 @@ export interface CoreStart {
// (undocumented)
docLinks: DocLinksStart;
// (undocumented)
executionContext: ExecutionContextStart;
// (undocumented)
fatalErrors: FatalErrorsStart;
// (undocumented)
http: HttpStart;
@ -461,6 +465,7 @@ export class CoreSystem {
// (undocumented)
start(): Promise<{
application: InternalApplicationStart;
executionContext: ExecutionContextSetup;
} | undefined>;
// (undocumented)
stop(): void;
@ -511,6 +516,20 @@ export interface ErrorToastOptions extends ToastOptions {
toastMessage?: string;
}
// @public
export interface ExecutionContextSetup {
clear(): void;
context$: Observable<KibanaExecutionContext>;
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)
@ -751,9 +770,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;
@ -1522,6 +1542,6 @@ export interface UserProvidedValues<T = any> {
// 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
```

View file

@ -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;
}

View file

@ -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<ExecutionContextSetup> = {
withContext: jest.fn(),
getAsLabels: jest.fn(),
};
mock.withContext.mockImplementation(withContextMock);
return mock;

View file

@ -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<R>(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
);
}
}

View file

@ -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);

View file

@ -161,6 +161,7 @@ export function createPluginSetupContext<TPlugin, TPluginDependencies>(
},
executionContext: {
withContext: deps.executionContext.withContext,
getAsLabels: deps.executionContext.getAsLabels,
},
http: {
createCookieSessionStorageFactory: deps.http.createCookieSessionStorageFactory,

View file

@ -7,6 +7,7 @@
/// <reference types="node" />
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';
@ -994,6 +995,8 @@ export class EventLoopDelaysMonitor {
// @public (undocumented)
export interface ExecutionContextSetup {
// (undocumented)
getAsLabels(): apm.Labels;
withContext<R>(context: KibanaExecutionContext | undefined, fn: (...args: any[]) => R): R;
}
@ -1319,9 +1322,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;

View file

@ -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<typeof injectedMetadataServiceMock.createSetupContract>,
@ -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 };
}

View file

@ -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 */

View file

@ -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,

View file

@ -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) {

View file

@ -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([

View file

@ -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',
},
})
);

View file

@ -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<IKibanaSearchResponse>;
} else {
const { executionContext, ...rest } = options || {};
return this.batchedFetch(
{
request,
options: this.getSerializableOptions(options),
options: this.getSerializableOptions({
...rest,
executionContext: this.deps.executionContext.withGlobalContext(executionContext),
}),
},
abortSignal
);

View file

@ -89,7 +89,7 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
constructor(private initializerContext: PluginInitializerContext<ConfigSchema>) {}
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<ISearchSetup, ISearchStart> {
this.searchInterceptor = new SearchInterceptor({
bfetch,
toasts: notifications.toasts,
executionContext,
http,
uiSettings,
startServices: getStartServices(),

View file

@ -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();
});
},
};
});

View file

@ -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 (
<main className="devApp">
<EuiTabs style={{ paddingLeft: euiThemeVars.euiSizeS }} size="l">

View file

@ -61,7 +61,7 @@ export class DevToolsPlugin implements Plugin<DevToolsSetup, void> {
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<DevToolsSetup, void> {
const appServices = {
breadcrumbService: this.breadcrumbService,
docTitleService: this.docTitleService,
executionContext,
};
const { renderApp } = await import('./application');

View file

@ -46,6 +46,9 @@ describe('ContextApp test', () => {
toastNotifications: { addDanger: () => {} },
navigation: mockNavigationPlugin,
core: {
executionContext: {
set: jest.fn(),
},
notifications: { toasts: [] },
theme: { theme$: themeServiceMock.createStartContract().theme$ },
},

View file

@ -25,6 +25,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';
import { generateFilters } from '../../../../data/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
*/

View file

@ -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<DocUrlParams>();
const breadcrumb = useMainRouteBreadcrumb();
useExecutionContext(core.executionContext, {
type: 'application',
page: 'single-doc',
id: indexPatternId,
});
useEffect(() => {
chrome.setBreadcrumbs([
...getRootBreadcrumbs(breadcrumb),

View file

@ -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<DiscoverLandingParams>();
useExecutionContext(core.executionContext, {
type: 'application',
page: 'app',
id: id || 'new',
});
const navigateToOverview = useCallback(() => {
core.application.navigateToApp('kibanaOverview', { path: '#' });
}, [core.application]);

View file

@ -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": "/",
}
`);
});

View file

@ -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

View file

@ -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": "/",
}
`);
});

View file

@ -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

View file

@ -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": "/",
}
`);
});

View file

@ -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

View file

@ -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';

View file

@ -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';

View file

@ -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]);
}

View file

@ -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';
@ -398,20 +398,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: {

View file

@ -36,6 +36,7 @@ import {
setDocLinks,
setSpaces,
setTheme,
setExecutionContext,
} from './services';
import {
createVisEmbeddableFromObject,
@ -372,6 +373,7 @@ export class VisualizationsPlugin
setTimeFilter(data.query.timefilter.timefilter);
setAggs(data.search.aggs);
setOverlays(core.overlays);
setExecutionContext(core.executionContext);
setChrome(core.chrome);
if (spaces) {

View file

@ -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<OverlayStart>('Over
export const [getChrome, setChrome] = createGetterSetter<ChromeStart>('Chrome');
export const [getExecutionContext, setExecutionContext] =
createGetterSetter<ExecutionContextSetup>('ExecutionContext');
export const [getSpaces, setSpaces] = createGetterSetter<SpacesPluginStart>('Spaces', false);

View file

@ -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,

View file

@ -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

View file

@ -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<string, any>) => {},
getAsLabels: () => {
return {};
},
withGlobalContext: () => {
return {};
},
};
return exec;
};

View file

@ -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 }),

View file

@ -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());

View file

@ -13,7 +13,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';
@ -71,6 +71,7 @@ export function App({
getOriginatingAppName,
spaces,
http,
executionContext,
// Temporarily required until the 'by value' paradigm is default.
dashboardFeatureFlag,
} = lensAppServices;
@ -111,6 +112,7 @@ export function App({
undefined
);
const [isGoBackToVizEditorModalVisible, setIsGoBackToVizEditorModalVisible] = useState(false);
const savedObjectId = (initialInput as LensByReferenceInput)?.savedObjectId;
useEffect(() => {
if (currentDoc) {
@ -122,6 +124,12 @@ export function App({
setIndicateNoData(true);
}, [setIndicateNoData]);
useExecutionContext(executionContext, {
type: 'application',
id: savedObjectId || 'new',
page: 'editor',
});
useEffect(() => {
if (indicateNoData) {
setIndicateNoData(false);
@ -132,11 +140,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(() => {

View file

@ -77,6 +77,7 @@ export async function getLensServices(
usageCollection,
savedObjectsTagging,
attributeService,
executionContext: coreStart.executionContext,
http: coreStart.http,
chrome: coreStart.chrome,
overlays: coreStart.overlays,

View file

@ -12,6 +12,7 @@ import type {
ApplicationStart,
AppMountParameters,
ChromeStart,
ExecutionContextStart,
HttpStart,
IUiSettingsClient,
NotificationsStart,
@ -114,6 +115,7 @@ export interface HistoryLocationState {
export interface LensAppServices {
http: HttpStart;
executionContext: ExecutionContextStart;
chrome: ChromeStart;
overlays: OverlayStart;
storage: IStorageWrapper;

View file

@ -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(),

View file

@ -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',
});
});
});

View file

@ -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
);
}

View file

@ -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(() => {

View file

@ -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;

View file

@ -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());

View file

@ -16,6 +16,7 @@ import { type Filter, FilterStateStore } from '@kbn/es-query';
import type { Query, TimeRange, DataView } from 'src/plugins/data/common';
import {
getData,
getExecutionContext,
getCoreChrome,
getMapsCapabilities,
getNavigation,
@ -115,6 +116,12 @@ export class MapApp extends React.Component<Props, State> {
componentDidMount() {
this._isMounted = true;
getExecutionContext().set({
type: 'application',
page: 'editor',
id: this.props.savedMap.getSavedObjectId() || 'new',
});
this._autoRefreshSubscription = getTimeFilter()
.getAutoRefreshFetch$()
.pipe(

View file

@ -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: '/',
},
});
});
});

View file

@ -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,
};
}

View file

@ -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(
{

View file

@ -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(
{