[tags] add performance journey to track CRUD operations on listing page (#164537)

## Summary

This PR adds single user performance journey to track CRUD operations on
Tags listing page:
- get all tags on initial loading
- create a new tag 
- update the tag
- delete the tag
- bulk delete (first 20 tags)

flaky-test-runner 25x:
https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/2966

Added visualisations to monitor metrics, most operations take <1 second
<img width="523" alt="image"
src="f4c14e49-edf6-4fff-9f31-30b8a67970e9">

Since bulk delete takes ~20 sec, I put it on the separate visualisation
<img width="523" alt="Screenshot 2023-08-23 at 18 19 46"
src="467983f8-f8eb-486a-8e27-beac0d9b1f37">


dd0473ac-826f-5621-9a10-25319700326e?_g=h@3b0c329

To run locally: `node scripts/functional_tests.js --config
x-pack/performance/journeys/tags_listing_page.ts`

Note: this journey is compatible to be executed on Serverless project

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Dzmitry Lemechko 2023-08-25 14:46:32 +02:00 committed by GitHub
parent 4180a1a105
commit 4552c6e3b7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 5997 additions and 15 deletions

View file

@ -419,6 +419,7 @@ enabled:
- x-pack/performance/journeys/ecommerce_dashboard_saved_search_only.ts
- x-pack/performance/journeys/ecommerce_dashboard_tsvb_gauge_only.ts
- x-pack/performance/journeys/dashboard_listing_page.ts
- x-pack/performance/journeys/tags_listing_page.ts
- x-pack/performance/journeys/cloud_security_dashboard.ts
- x-pack/performance/journeys/apm_service_inventory.ts
- x-pack/test/custom_branding/config.ts

View file

@ -72,4 +72,18 @@ export class KibanaPage {
checkAttribute: 'data-ech-render-complete',
});
}
async clearInput(locator: string) {
const textArea = this.page.locator(locator);
await textArea.clear();
}
async clickAndWaitFor(
locator: string,
state?: 'attached' | 'detached' | 'visible' | 'hidden' | undefined
) {
const element = this.page.locator(locator);
await element.click();
await element.waitFor({ state });
}
}

View file

@ -0,0 +1,64 @@
/*
* 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 { Journey } from '@kbn/journeys';
import { subj } from '@kbn/test-subj-selector';
const TAG_NAME = 'testing';
const TAG_DESCRIPTION = 'test description';
export const journey = new Journey({
esArchives: ['x-pack/performance/es_archives/sample_data_flights'],
kbnArchives: ['x-pack/performance/kbn_archives/many_tags_and_visualizations'],
})
.step('Go to Tags Page', async ({ page, kbnUrl }) => {
await page.goto(kbnUrl.get(`/app/management/kibana/tags`));
await page.waitForSelector(subj('tagsManagementTable table-is-ready'));
})
.step('Delete the first 20 tags', async ({ page }) => {
await page.click(subj('checkboxSelectAll'));
await page.click(subj('actionBar-contextMenuButton'));
await page.click(subj('actionBar-button-delete'));
await page.click(subj('confirmModalConfirmButton'));
await page.waitForSelector(subj('tagsManagementTable table-is-ready'));
})
.step(`Search for 'stream' tag`, async ({ page, inputDelays }) => {
await page.type(subj('tagsManagementSearchBar'), 'stream', {
delay: inputDelays.TYPING,
});
await page.waitForSelector(subj('tagsManagementTable table-is-ready'));
})
.step('Create a new tag', async ({ page, inputDelays, kibanaPage }) => {
await kibanaPage.clearInput(subj('tagsManagementSearchBar'));
await page.waitForSelector(subj('tagsManagementTable table-is-ready'));
await page.click(subj('createTagButton'));
await page.type(subj('createModalField-name'), TAG_NAME, { delay: inputDelays.TYPING });
await kibanaPage.clickAndWaitFor(subj('createModalConfirmButton'), 'detached');
await page.waitForSelector(subj('tagsManagementTable table-is-ready'));
// search for newly created tag
await page.type(subj('tagsManagementSearchBar'), TAG_NAME, {
delay: inputDelays.TYPING,
});
await page.waitForSelector(subj('tagsManagementTable table-is-ready'));
await page.waitForSelector(subj('tagsTableRowName'), { state: 'visible' });
})
.step('Update tag', async ({ page, inputDelays, kibanaPage }) => {
await page.click(subj('tagsTableAction-edit'));
await page.type(subj('createModalField-description'), TAG_DESCRIPTION, {
delay: inputDelays.TYPING,
});
await kibanaPage.clickAndWaitFor(subj('createModalConfirmButton'), 'detached');
await page.waitForSelector(subj('tagsManagementTable table-is-ready'));
})
.step('Delete tag', async ({ page }) => {
const tagRow = page.locator(subj('tagsTableRowName'));
await page.click(subj('euiCollapsedItemActionsButton'));
await page.click(subj('tagsTableAction-delete'));
await page.click(subj('confirmModalConfirmButton'));
await page.waitForSelector(subj('tagsManagementTable table-is-ready'));
await tagRow.waitFor({ state: 'detached' });
});

File diff suppressed because one or more lines are too long

View file

@ -139,9 +139,11 @@ export const TagTable: FC<TagTableProps> = ({
: []),
];
const testSubjectState = !loading ? 'table-is-ready' : 'table-is-loading';
return (
<EuiInMemoryTable
data-test-subj="tagsManagementTable"
data-test-subj={`tagsManagementTable ${testSubjectState}`}
ref={tableRef}
childrenBetween={actionBar}
loading={loading}

View file

@ -67,12 +67,12 @@ export class SavedObjectTaggingPlugin
return {};
}
public start({ http, application, overlays, theme }: CoreStart) {
public start({ http, application, overlays, theme, analytics }: CoreStart) {
this.tagCache = new TagsCache({
refreshHandler: () => this.tagClient!.getAll({ asSystemRequest: true }),
refreshInterval: this.config.cacheRefreshInterval,
});
this.tagClient = new TagsClient({ http, changeListener: this.tagCache });
this.tagClient = new TagsClient({ analytics, http, changeListener: this.tagCache });
this.assignmentService = new TagAssignmentService({ http });
// do not fetch tags on anonymous page

View file

@ -10,6 +10,7 @@ import { Tag } from '../../../common/types';
import { createTag, createTagAttributes } from '../../../common/test_utils';
import { tagsCacheMock } from './tags_cache.mock';
import { TagsClient, FindTagsOptions } from './tags_client';
import { coreMock } from '@kbn/core/public/mocks';
describe('TagsClient', () => {
let tagsClient: TagsClient;
@ -19,7 +20,9 @@ describe('TagsClient', () => {
beforeEach(() => {
http = httpServiceMock.createSetupContract();
changeListener = tagsCacheMock.create();
const { analytics } = coreMock.createStart();
tagsClient = new TagsClient({
analytics,
http,
changeListener,
});

View file

@ -5,7 +5,8 @@
* 2.0.
*/
import { HttpSetup } from '@kbn/core/public';
import { HttpSetup, AnalyticsServiceStart } from '@kbn/core/public';
import { reportPerformanceMetricEvent } from '@kbn/ebt-tools';
import {
Tag,
TagAttributes,
@ -15,7 +16,15 @@ import {
} from '../../../common/types';
import { ITagsChangeListener } from './tags_cache';
const BULK_DELETE_TAG_EVENT = 'bulkDeleteTag';
const CREATE_TAG_EVENT = 'createTag';
const DELETE_TAG_EVENT = 'deleteTag';
const GET_ALL_TAGS_EVENT = 'getAllTag';
const FIND_TAG_EVENT = 'findTag';
const UPDATE_TAG_EVENT = 'updateTag';
export interface TagsClientOptions {
analytics: AnalyticsServiceStart;
http: HttpSetup;
changeListener?: ITagsChangeListener;
}
@ -45,10 +54,12 @@ export interface ITagInternalClient extends ITagsClient {
}
export class TagsClient implements ITagInternalClient {
private readonly analytics: AnalyticsServiceStart;
private readonly http: HttpSetup;
private readonly changeListener?: ITagsChangeListener;
constructor({ http, changeListener }: TagsClientOptions) {
constructor({ analytics, http, changeListener }: TagsClientOptions) {
this.analytics = analytics;
this.http = http;
this.changeListener = changeListener;
}
@ -56,9 +67,15 @@ export class TagsClient implements ITagInternalClient {
// public APIs from ITagsClient
public async create(attributes: TagAttributes) {
const startTime = window.performance.now();
const { tag } = await this.http.post<{ tag: Tag }>('/api/saved_objects_tagging/tags/create', {
body: JSON.stringify(attributes),
});
const duration = window.performance.now() - startTime;
reportPerformanceMetricEvent(this.analytics, {
eventName: CREATE_TAG_EVENT,
duration,
});
trapErrors(() => {
if (this.changeListener) {
@ -70,9 +87,15 @@ export class TagsClient implements ITagInternalClient {
}
public async update(id: string, attributes: TagAttributes) {
const startTime = window.performance.now();
const { tag } = await this.http.post<{ tag: Tag }>(`/api/saved_objects_tagging/tags/${id}`, {
body: JSON.stringify(attributes),
});
const duration = window.performance.now() - startTime;
reportPerformanceMetricEvent(this.analytics, {
eventName: UPDATE_TAG_EVENT,
duration,
});
trapErrors(() => {
if (this.changeListener) {
@ -90,11 +113,17 @@ export class TagsClient implements ITagInternalClient {
}
public async getAll({ asSystemRequest }: GetAllTagsOptions = {}) {
const startTime = window.performance.now();
const fetchOptions = { asSystemRequest };
const { tags } = await this.http.get<{ tags: Tag[] }>(
'/api/saved_objects_tagging/tags',
fetchOptions
);
const duration = window.performance.now() - startTime;
reportPerformanceMetricEvent(this.analytics, {
eventName: GET_ALL_TAGS_EVENT,
duration,
});
trapErrors(() => {
if (this.changeListener) {
@ -106,7 +135,13 @@ export class TagsClient implements ITagInternalClient {
}
public async delete(id: string) {
const startTime = window.performance.now();
await this.http.delete<{}>(`/api/saved_objects_tagging/tags/${id}`);
const duration = window.performance.now() - startTime;
reportPerformanceMetricEvent(this.analytics, {
eventName: DELETE_TAG_EVENT,
duration,
});
trapErrors(() => {
if (this.changeListener) {
@ -118,21 +153,38 @@ export class TagsClient implements ITagInternalClient {
// internal APIs from ITagInternalClient
public async find({ page, perPage, search }: FindTagsOptions) {
return await this.http.get<FindTagsResponse>('/internal/saved_objects_tagging/tags/_find', {
query: {
page,
perPage,
search,
},
const startTime = window.performance.now();
const response = await this.http.get<FindTagsResponse>(
'/internal/saved_objects_tagging/tags/_find',
{
query: {
page,
perPage,
search,
},
}
);
const duration = window.performance.now() - startTime;
reportPerformanceMetricEvent(this.analytics, {
eventName: FIND_TAG_EVENT,
duration,
});
return response;
}
public async bulkDelete(tagIds: string[]) {
const startTime = window.performance.now();
await this.http.post<{}>('/internal/saved_objects_tagging/tags/_bulk_delete', {
body: JSON.stringify({
ids: tagIds,
}),
});
const duration = window.performance.now() - startTime;
reportPerformanceMetricEvent(this.analytics, {
eventName: BULK_DELETE_TAG_EVENT,
duration,
});
trapErrors(() => {
if (this.changeListener) {

View file

@ -21,6 +21,7 @@
"@kbn/utility-types",
"@kbn/i18n-react",
"@kbn/config-schema",
"@kbn/ebt-tools",
],
"exclude": [
"target/**/*",

View file

@ -270,10 +270,7 @@ export class TagManagementPageObject extends FtrService {
*/
async waitUntilTableIsLoaded() {
return this.retry.try(async () => {
const isLoaded = await this.find.existsByDisplayedByCssSelector(
'*[data-test-subj="tagsManagementTable"]:not(.euiBasicTable-loading)'
);
const isLoaded = await this.testSubjects.exists('tagsManagementTable table-is-ready');
if (isLoaded) {
return true;
} else {