From 6398e22b4f9bbc6d01f053187e52e86738a57c3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Tue, 7 Jan 2020 08:33:49 +0000 Subject: [PATCH 1/7] adding message to transaction and span metadata (#54017) --- .../__test__/SpanMetadata.test.tsx | 18 +++++++++++++----- .../MetadataTable/SpanMetadata/sections.ts | 4 +++- .../__test__/TransactionMetadata.test.tsx | 18 ++++++++++++++---- .../TransactionMetadata/sections.ts | 4 +++- .../shared/MetadataTable/sections.ts | 17 +++++++++++++++++ .../apm/typings/es_schemas/raw/SpanRaw.ts | 6 ++++++ .../typings/es_schemas/raw/TransactionRaw.ts | 6 ++++++ 7 files changed, 62 insertions(+), 11 deletions(-) diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/__test__/SpanMetadata.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/__test__/SpanMetadata.test.tsx index 4b6355034f16a..99d8a0790a816 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/__test__/SpanMetadata.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/__test__/SpanMetadata.test.tsx @@ -31,11 +31,15 @@ describe('SpanMetadata', () => { name: 'opbeans-java' }, span: { - id: '7efbc7056b746fcb' + id: '7efbc7056b746fcb', + message: { + age: { ms: 1577958057123 }, + queue: { name: 'queue name' } + } } } as unknown) as Span; const output = render(, renderOptions); - expectTextsInDocument(output, ['Service', 'Agent']); + expectTextsInDocument(output, ['Service', 'Agent', 'Message']); }); }); describe('when a span is presented', () => { @@ -55,11 +59,15 @@ describe('SpanMetadata', () => { response: { status_code: 200 } }, subtype: 'http', - type: 'external' + type: 'external', + message: { + age: { ms: 1577958057123 }, + queue: { name: 'queue name' } + } } } as unknown) as Span; const output = render(, renderOptions); - expectTextsInDocument(output, ['Service', 'Agent', 'Span']); + expectTextsInDocument(output, ['Service', 'Agent', 'Span', 'Message']); }); }); describe('when there is no id inside span', () => { @@ -83,7 +91,7 @@ describe('SpanMetadata', () => { } as unknown) as Span; const output = render(, renderOptions); expectTextsInDocument(output, ['Service', 'Agent']); - expectTextsNotInDocument(output, ['Span']); + expectTextsNotInDocument(output, ['Span', 'Message']); }); }); }); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/sections.ts b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/sections.ts index 7012bbcc8fcea..5a83a9bf4ef9e 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/sections.ts +++ b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/sections.ts @@ -11,7 +11,8 @@ import { SPAN, LABELS, TRANSACTION, - TRACE + TRACE, + MESSAGE_SPAN } from '../sections'; export const SPAN_METADATA_SECTIONS: Section[] = [ @@ -20,5 +21,6 @@ export const SPAN_METADATA_SECTIONS: Section[] = [ TRANSACTION, TRACE, SERVICE, + MESSAGE_SPAN, AGENT ]; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/__test__/TransactionMetadata.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/__test__/TransactionMetadata.test.tsx index 1fcb093fa0354..93e87e884ea76 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/__test__/TransactionMetadata.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/__test__/TransactionMetadata.test.tsx @@ -35,6 +35,10 @@ function getTransaction() { notIncluded: 'transaction not included value', custom: { someKey: 'custom value' + }, + message: { + age: { ms: 1577958057123 }, + queue: { name: 'queue name' } } } } as unknown) as Transaction; @@ -59,7 +63,8 @@ describe('TransactionMetadata', () => { 'Agent', 'URL', 'User', - 'Custom' + 'Custom', + 'Message' ]); }); @@ -81,7 +86,9 @@ describe('TransactionMetadata', () => { 'agent.someKey', 'url.someKey', 'user.someKey', - 'transaction.custom.someKey' + 'transaction.custom.someKey', + 'transaction.message.age.ms', + 'transaction.message.queue.name' ]); // excluded keys @@ -109,7 +116,9 @@ describe('TransactionMetadata', () => { 'agent value', 'url value', 'user value', - 'custom value' + 'custom value', + '1577958057123', + 'queue name' ]); // excluded values @@ -138,7 +147,8 @@ describe('TransactionMetadata', () => { 'Process', 'Agent', 'URL', - 'Custom' + 'Custom', + 'Message' ]); }); }); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/sections.ts b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/sections.ts index 6b30c82bc35a0..18751efc6e1c1 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/sections.ts +++ b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/sections.ts @@ -18,7 +18,8 @@ import { PAGE, USER, USER_AGENT, - CUSTOM_TRANSACTION + CUSTOM_TRANSACTION, + MESSAGE_TRANSACTION } from '../sections'; export const TRANSACTION_METADATA_SECTIONS: Section[] = [ @@ -29,6 +30,7 @@ export const TRANSACTION_METADATA_SECTIONS: Section[] = [ CONTAINER, SERVICE, PROCESS, + MESSAGE_TRANSACTION, AGENT, URL, { ...PAGE, key: 'transaction.page' }, diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/sections.ts b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/sections.ts index 403663ce2095a..ac8e9559357e3 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/sections.ts +++ b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/sections.ts @@ -136,3 +136,20 @@ export const CUSTOM_TRANSACTION: Section = { key: 'transaction.custom', label: customLabel }; + +const messageLabel = i18n.translate( + 'xpack.apm.metadataTable.section.messageLabel', + { + defaultMessage: 'Message' + } +); + +export const MESSAGE_TRANSACTION: Section = { + key: 'transaction.message', + label: messageLabel +}; + +export const MESSAGE_SPAN: Section = { + key: 'span.message', + label: messageLabel +}; diff --git a/x-pack/legacy/plugins/apm/typings/es_schemas/raw/SpanRaw.ts b/x-pack/legacy/plugins/apm/typings/es_schemas/raw/SpanRaw.ts index 5ba480221c997..60e523f1aa043 100644 --- a/x-pack/legacy/plugins/apm/typings/es_schemas/raw/SpanRaw.ts +++ b/x-pack/legacy/plugins/apm/typings/es_schemas/raw/SpanRaw.ts @@ -40,6 +40,12 @@ export interface SpanRaw extends APMBaseDoc { statement?: string; type?: string; }; + message?: { + queue?: { name: string }; + age?: { ms: number }; + body?: string; + headers?: Record; + }; }; transaction?: { id: string; diff --git a/x-pack/legacy/plugins/apm/typings/es_schemas/raw/TransactionRaw.ts b/x-pack/legacy/plugins/apm/typings/es_schemas/raw/TransactionRaw.ts index ce7c11f34a220..4dc5f8c897c26 100644 --- a/x-pack/legacy/plugins/apm/typings/es_schemas/raw/TransactionRaw.ts +++ b/x-pack/legacy/plugins/apm/typings/es_schemas/raw/TransactionRaw.ts @@ -43,6 +43,12 @@ export interface TransactionRaw extends APMBaseDoc { }; type: string; custom?: Record; + message?: { + queue?: { name: string }; + age?: { ms: number }; + body?: string; + headers?: Record; + }; }; // Shared by errors and transactions From e687fc63dfe815877378299f2b9f7c443ccf6088 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Tue, 7 Jan 2020 11:01:21 +0100 Subject: [PATCH 2/7] [Console] Telemetry (part 1) (#52893) * Saving anonymised data to SO * Add new files * Hook up usage collector * Added app start up ui metric tracking * Only use client side track metrics functionality * Added comment regarding use of `patterns`, renamed trackMetric -> trackUiMetric * Fix jest tests * Slight refactor and fix for functional tests. More defensive tracking logic * Fix types in test * Minor refactor to get endpoint description - removed SenseEditor from autocomplete. Fix bug where cursor at end of line does not get endpoint informaiton * Send request to es: do not mutate args Always move cursor to end of line when getting endpoint description * Create an interface a simple interface to the metrics tracker Use the new createUiStatsReporter function to create the tracker Co-authored-by: Elastic Machine --- .../core_plugins/console/public/kibana.json | 3 +- .../core_plugins/console/public/legacy.ts | 10 +++-- .../console_editor/editor.test.mock.tsx | 4 ++ .../legacy/console_editor/editor.test.tsx | 16 ++++++-- .../editor/legacy/console_menu_actions.ts | 5 +-- .../np_ready/application/contexts/index.ts | 2 +- .../application/contexts/services_context.tsx | 4 +- .../send_request_to_es.ts | 3 +- .../use_send_current_request_to_es/track.ts | 40 ++++++++++++++++++ .../use_send_current_request_to_es.ts | 16 ++++---- .../public/np_ready/application/index.tsx | 12 +++++- .../application/models/sense_editor/index.ts | 1 + .../np_ready/lib/autocomplete/autocomplete.ts | 35 ++++++---------- .../get_endpoint_from_position.ts | 41 +++++++++++++++++++ .../console/public/np_ready/lib/es/es.js | 1 - .../public/np_ready/services/tracker.ts | 31 ++++++++++++++ .../console/public/np_ready/types/common.ts | 5 +++ .../console/public/np_ready/types/index.ts | 1 + .../spec/overrides/sql.query.json | 3 +- 19 files changed, 186 insertions(+), 47 deletions(-) create mode 100644 src/legacy/core_plugins/console/public/np_ready/application/hooks/use_send_current_request_to_es/track.ts create mode 100644 src/legacy/core_plugins/console/public/np_ready/lib/autocomplete/get_endpoint_from_position.ts create mode 100644 src/legacy/core_plugins/console/public/np_ready/services/tracker.ts diff --git a/src/legacy/core_plugins/console/public/kibana.json b/src/legacy/core_plugins/console/public/kibana.json index 3363af353912a..c58a5a90fb9f2 100644 --- a/src/legacy/core_plugins/console/public/kibana.json +++ b/src/legacy/core_plugins/console/public/kibana.json @@ -3,5 +3,6 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": ["home"] + "requiredPlugins": ["home"], + "optionalPlugins": ["usageCollection"] } diff --git a/src/legacy/core_plugins/console/public/legacy.ts b/src/legacy/core_plugins/console/public/legacy.ts index c456d777187aa..d151a27d27e5c 100644 --- a/src/legacy/core_plugins/console/public/legacy.ts +++ b/src/legacy/core_plugins/console/public/legacy.ts @@ -22,7 +22,13 @@ import { I18nContext } from 'ui/i18n'; import chrome from 'ui/chrome'; import { FeatureCatalogueCategory } from 'ui/registry/feature_catalogue'; +import { plugin } from './np_ready'; +import { DevToolsSetup } from '../../../../plugins/dev_tools/public'; +import { HomePublicPluginSetup } from '../../../../plugins/home/public'; +import { UsageCollectionSetup } from '../../../../plugins/usage_collection/public'; + export interface XPluginSet { + usageCollection: UsageCollectionSetup; dev_tools: DevToolsSetup; home: HomePublicPluginSetup; __LEGACY: { @@ -32,10 +38,6 @@ export interface XPluginSet { }; } -import { plugin } from './np_ready'; -import { DevToolsSetup } from '../../../../plugins/dev_tools/public'; -import { HomePublicPluginSetup } from '../../../../plugins/home/public'; - const pluginInstance = plugin({} as any); (async () => { diff --git a/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_editor/editor.test.mock.tsx b/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_editor/editor.test.mock.tsx index 5df72c0f03496..0ee7998d331f5 100644 --- a/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_editor/editor.test.mock.tsx +++ b/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_editor/editor.test.mock.tsx @@ -22,6 +22,7 @@ jest.mock('../../../../contexts/editor_context/editor_registry.ts', () => ({ setInputEditor: () => {}, getInputEditor: () => ({ getRequestsInRange: async () => [{ test: 'test' }], + getCoreEditor: () => ({ getCurrentPosition: jest.fn() }), }), }, })); @@ -52,3 +53,6 @@ jest.mock('../../../../models/sense_editor', () => { jest.mock('../../../../hooks/use_send_current_request_to_es/send_request_to_es', () => ({ sendRequestToES: jest.fn(), })); +jest.mock('../../../../../lib/autocomplete/get_endpoint_from_position', () => ({ + getEndpointFromPosition: jest.fn(), +})); diff --git a/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_editor/editor.test.tsx b/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_editor/editor.test.tsx index 6162397ce0650..73ee6d160613f 100644 --- a/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_editor/editor.test.tsx +++ b/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_editor/editor.test.tsx @@ -32,14 +32,18 @@ import { ServicesContextProvider, EditorContextProvider, RequestContextProvider, + ContextValue, } from '../../../../contexts'; +// Mocked functions import { sendRequestToES } from '../../../../hooks/use_send_current_request_to_es/send_request_to_es'; +import { getEndpointFromPosition } from '../../../../../lib/autocomplete/get_endpoint_from_position'; + import * as consoleMenuActions from '../console_menu_actions'; import { Editor } from './editor'; describe('Legacy (Ace) Console Editor Component Smoke Test', () => { - let mockedAppContextValue: any; + let mockedAppContextValue: ContextValue; const sandbox = sinon.createSandbox(); const doMount = () => @@ -58,11 +62,15 @@ describe('Legacy (Ace) Console Editor Component Smoke Test', () => { beforeEach(() => { document.queryCommandSupported = sinon.fake(() => true); mockedAppContextValue = { + elasticsearchUrl: 'test', services: { + trackUiMetric: { count: () => {}, load: () => {} }, + settings: {} as any, + storage: {} as any, history: { - getSavedEditorState: () => null, + getSavedEditorState: () => ({} as any), updateCurrentState: jest.fn(), - }, + } as any, notifications: notificationServiceMock.createSetupContract(), }, docLinkVersion: 'NA', @@ -70,10 +78,12 @@ describe('Legacy (Ace) Console Editor Component Smoke Test', () => { }); afterEach(() => { + jest.clearAllMocks(); sandbox.restore(); }); it('calls send current request to ES', async () => { + (getEndpointFromPosition as jest.Mock).mockReturnValue({ patterns: [] }); (sendRequestToES as jest.Mock).mockRejectedValue({}); const editor = doMount(); act(() => { diff --git a/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_menu_actions.ts b/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_menu_actions.ts index 797ff5744eec3..2bbe49cd53eac 100644 --- a/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_menu_actions.ts +++ b/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_menu_actions.ts @@ -17,8 +17,7 @@ * under the License. */ -// @ts-ignore -import { getEndpointFromPosition } from '../../../../lib/autocomplete/autocomplete'; +import { getEndpointFromPosition } from '../../../../lib/autocomplete/get_endpoint_from_position'; import { SenseEditor } from '../../../models/sense_editor'; export async function autoIndent(editor: SenseEditor, event: Event) { @@ -40,7 +39,7 @@ export function getDocumentation( } const position = requests[0].range.end; position.column = position.column - 1; - const endpoint = getEndpointFromPosition(editor, position, editor.parser); + const endpoint = getEndpointFromPosition(editor.getCoreEditor(), position, editor.parser); if (endpoint && endpoint.documentation && endpoint.documentation.indexOf('http') !== -1) { return endpoint.documentation .replace('/master/', `/${docLinkVersion}/`) diff --git a/src/legacy/core_plugins/console/public/np_ready/application/contexts/index.ts b/src/legacy/core_plugins/console/public/np_ready/application/contexts/index.ts index 18234acf15957..e489bd50c9ce0 100644 --- a/src/legacy/core_plugins/console/public/np_ready/application/contexts/index.ts +++ b/src/legacy/core_plugins/console/public/np_ready/application/contexts/index.ts @@ -17,7 +17,7 @@ * under the License. */ -export { useServicesContext, ServicesContextProvider } from './services_context'; +export { useServicesContext, ServicesContextProvider, ContextValue } from './services_context'; export { useRequestActionContext, diff --git a/src/legacy/core_plugins/console/public/np_ready/application/contexts/services_context.tsx b/src/legacy/core_plugins/console/public/np_ready/application/contexts/services_context.tsx index 77f0924a51842..f14685ecd4ac7 100644 --- a/src/legacy/core_plugins/console/public/np_ready/application/contexts/services_context.tsx +++ b/src/legacy/core_plugins/console/public/np_ready/application/contexts/services_context.tsx @@ -20,13 +20,15 @@ import React, { createContext, useContext } from 'react'; import { NotificationsSetup } from 'kibana/public'; import { History, Storage, Settings } from '../../services'; +import { MetricsTracker } from '../../types'; -interface ContextValue { +export interface ContextValue { services: { history: History; storage: Storage; settings: Settings; notifications: NotificationsSetup; + trackUiMetric: MetricsTracker; }; elasticsearchUrl: string; docLinkVersion: string; diff --git a/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_send_current_request_to_es/send_request_to_es.ts b/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_send_current_request_to_es/send_request_to_es.ts index 11c1f6638e9cf..10dab65b61d44 100644 --- a/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_send_current_request_to_es/send_request_to_es.ts +++ b/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_send_current_request_to_es/send_request_to_es.ts @@ -39,7 +39,8 @@ export interface ESRequestResult { } let CURRENT_REQ_ID = 0; -export function sendRequestToES({ requests }: EsRequestArgs): Promise { +export function sendRequestToES(args: EsRequestArgs): Promise { + const requests = args.requests.slice(); return new Promise((resolve, reject) => { const reqId = ++CURRENT_REQ_ID; const results: ESRequestResult[] = []; diff --git a/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_send_current_request_to_es/track.ts b/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_send_current_request_to_es/track.ts new file mode 100644 index 0000000000000..4d993512c8fa7 --- /dev/null +++ b/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_send_current_request_to_es/track.ts @@ -0,0 +1,40 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SenseEditor } from '../../models/sense_editor'; +import { getEndpointFromPosition } from '../../../lib/autocomplete/get_endpoint_from_position'; +import { MetricsTracker } from '../../../types'; + +export const track = (requests: any[], editor: SenseEditor, trackUiMetric: MetricsTracker) => { + const coreEditor = editor.getCoreEditor(); + // `getEndpointFromPosition` gets values from the server-side generated JSON files which + // are a combination of JS, automatically generated JSON and manual overrides. That means + // the metrics reported from here will be tied to the definitions in those files. + // See src/legacy/core_plugins/console/server/api_server/spec + const endpointDescription = getEndpointFromPosition( + coreEditor, + coreEditor.getCurrentPosition(), + editor.parser + ); + + if (requests[0] && endpointDescription) { + const eventName = `${requests[0].method}_${endpointDescription.id ?? 'unknown'}`; + trackUiMetric.count(eventName); + } +}; diff --git a/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts b/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts index b51c29f8e9db6..6bf0b5024376b 100644 --- a/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts +++ b/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts @@ -19,15 +19,16 @@ import { i18n } from '@kbn/i18n'; import { useCallback } from 'react'; import { instance as registry } from '../../contexts/editor_context/editor_registry'; -import { useServicesContext } from '../../contexts'; +import { useRequestActionContext, useServicesContext } from '../../contexts'; import { sendRequestToES } from './send_request_to_es'; -import { useRequestActionContext } from '../../contexts'; +import { track } from './track'; + // @ts-ignore import mappings from '../../../lib/mappings/mappings'; export const useSendCurrentRequestToES = () => { const { - services: { history, settings, notifications }, + services: { history, settings, notifications, trackUiMetric }, } = useServicesContext(); const dispatch = useRequestActionContext(); @@ -45,9 +46,10 @@ export const useSendCurrentRequestToES = () => { return; } - const results = await sendRequestToES({ - requests, - }); + // Fire and forget + setTimeout(() => track(requests, editor, trackUiMetric), 0); + + const results = await sendRequestToES({ requests }); results.forEach(({ request: { path, method, data } }) => { history.addToHistory(path, method, data); @@ -82,5 +84,5 @@ export const useSendCurrentRequestToES = () => { }); } } - }, [dispatch, settings, history, notifications]); + }, [dispatch, settings, history, notifications, trackUiMetric]); }; diff --git a/src/legacy/core_plugins/console/public/np_ready/application/index.tsx b/src/legacy/core_plugins/console/public/np_ready/application/index.tsx index 239e4320f00f8..89756513b2b22 100644 --- a/src/legacy/core_plugins/console/public/np_ready/application/index.tsx +++ b/src/legacy/core_plugins/console/public/np_ready/application/index.tsx @@ -22,6 +22,7 @@ import { NotificationsSetup } from 'kibana/public'; import { ServicesContextProvider, EditorContextProvider, RequestContextProvider } from './contexts'; import { Main } from './containers'; import { createStorage, createHistory, createSettings, Settings } from '../services'; +import { createUsageTracker } from '../services/tracker'; let settingsRef: Settings; export function legacyBackDoorToSettings() { @@ -36,6 +37,9 @@ export function boot(deps: { }) { const { I18nContext, notifications, docLinkVersion, elasticsearchUrl } = deps; + const trackUiMetric = createUsageTracker(); + trackUiMetric.load('opened_app'); + const storage = createStorage({ engine: window.localStorage, prefix: 'sense:', @@ -50,7 +54,13 @@ export function boot(deps: { value={{ elasticsearchUrl, docLinkVersion, - services: { storage, history, settings, notifications }, + services: { + storage, + history, + settings, + notifications, + trackUiMetric, + }, }} > diff --git a/src/legacy/core_plugins/console/public/np_ready/application/models/sense_editor/index.ts b/src/legacy/core_plugins/console/public/np_ready/application/models/sense_editor/index.ts index 9310de2724fbe..f2102d75685fd 100644 --- a/src/legacy/core_plugins/console/public/np_ready/application/models/sense_editor/index.ts +++ b/src/legacy/core_plugins/console/public/np_ready/application/models/sense_editor/index.ts @@ -21,3 +21,4 @@ export * from './create'; export * from '../legacy_core_editor/create_readonly'; export { MODE } from '../../../lib/row_parser'; export { SenseEditor } from './sense_editor'; +export { getEndpointFromPosition } from '../../../lib/autocomplete/get_endpoint_from_position'; diff --git a/src/legacy/core_plugins/console/public/np_ready/lib/autocomplete/autocomplete.ts b/src/legacy/core_plugins/console/public/np_ready/lib/autocomplete/autocomplete.ts index 7520807ca77f5..ac8fa1ea48caa 100644 --- a/src/legacy/core_plugins/console/public/np_ready/lib/autocomplete/autocomplete.ts +++ b/src/legacy/core_plugins/console/public/np_ready/lib/autocomplete/autocomplete.ts @@ -38,7 +38,6 @@ import { URL_PATH_END_MARKER } from './components/index'; import { createTokenIterator } from '../../application/factories'; import { Position, Token, Range, CoreEditor } from '../../types'; -import { SenseEditor } from '../../application/models/sense_editor'; let LAST_EVALUATED_TOKEN: any = null; @@ -54,11 +53,20 @@ function isUrlParamsToken(token: any) { return false; } } -function getCurrentMethodAndTokenPaths( + +/** + * Get the method and token paths for a specific position in the current editor buffer. + * + * This function can be used for getting autocomplete information or for getting more information + * about the endpoint associated with autocomplete. In future, these concerns should be better + * separated. + * + */ +export function getCurrentMethodAndTokenPaths( editor: CoreEditor, pos: Position, parser: any, - forceEndOfUrl?: boolean + forceEndOfUrl?: boolean /* Flag for indicating whether we want to avoid early escape optimization. */ ) { const tokenIter = createTokenIterator({ editor, @@ -186,7 +194,7 @@ function getCurrentMethodAndTokenPaths( } } - if (walkedSomeBody && (!bodyTokenPath || bodyTokenPath.length === 0)) { + if (walkedSomeBody && (!bodyTokenPath || bodyTokenPath.length === 0) && !forceEndOfUrl) { // we had some content and still no path -> the cursor is position after a closed body -> no auto complete return {}; } @@ -298,20 +306,6 @@ function getCurrentMethodAndTokenPaths( } return ret; } -export function getEndpointFromPosition(senseEditor: SenseEditor, pos: Position, parser: any) { - const editor = senseEditor.getCoreEditor(); - const context = { - ...getCurrentMethodAndTokenPaths( - editor, - { column: pos.column, lineNumber: pos.lineNumber }, - parser, - true - ), - }; - const components = getTopLevelUrlCompleteComponents(context.method); - populateContext(context.urlTokenPath, context, editor, true, components); - return context.endpoint; -} // eslint-disable-next-line export default function({ coreEditor: editor, parser }: { coreEditor: CoreEditor; parser: any }) { @@ -812,7 +806,6 @@ export default function({ coreEditor: editor, parser }: { coreEditor: CoreEditor if (!ret.urlTokenPath) { // zero length tokenPath is true - // console.log("Can't extract a valid url token path."); return context; } @@ -825,13 +818,11 @@ export default function({ coreEditor: editor, parser }: { coreEditor: CoreEditor ); if (!context.endpoint) { - // console.log("couldn't resolve an endpoint."); return context; } if (!ret.urlParamsTokenPath) { // zero length tokenPath is true - // console.log("Can't extract a valid urlParams token path."); return context; } let tokenPath: any[] = []; @@ -859,7 +850,6 @@ export default function({ coreEditor: editor, parser }: { coreEditor: CoreEditor context.requestStartRow = ret.requestStartRow; if (!ret.urlTokenPath) { // zero length tokenPath is true - // console.log("Can't extract a valid url token path."); return context; } @@ -875,7 +865,6 @@ export default function({ coreEditor: editor, parser }: { coreEditor: CoreEditor if (!ret.bodyTokenPath) { // zero length tokenPath is true - // console.log("Can't extract a valid body token path."); return context; } diff --git a/src/legacy/core_plugins/console/public/np_ready/lib/autocomplete/get_endpoint_from_position.ts b/src/legacy/core_plugins/console/public/np_ready/lib/autocomplete/get_endpoint_from_position.ts new file mode 100644 index 0000000000000..cb037e29e33f6 --- /dev/null +++ b/src/legacy/core_plugins/console/public/np_ready/lib/autocomplete/get_endpoint_from_position.ts @@ -0,0 +1,41 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CoreEditor, Position } from '../../types'; +import { getCurrentMethodAndTokenPaths } from './autocomplete'; + +// @ts-ignore +import { getTopLevelUrlCompleteComponents } from '../kb/kb'; +// @ts-ignore +import { populateContext } from './engine'; + +export function getEndpointFromPosition(editor: CoreEditor, pos: Position, parser: any) { + const lineValue = editor.getLineValue(pos.lineNumber); + const context = { + ...getCurrentMethodAndTokenPaths( + editor, + { column: lineValue.length, lineNumber: pos.lineNumber }, + parser, + true + ), + }; + const components = getTopLevelUrlCompleteComponents(context.method); + populateContext(context.urlTokenPath, context, editor, true, components); + return context.endpoint; +} diff --git a/src/legacy/core_plugins/console/public/np_ready/lib/es/es.js b/src/legacy/core_plugins/console/public/np_ready/lib/es/es.js index 9012b875e0f2b..e36976fb7acee 100644 --- a/src/legacy/core_plugins/console/public/np_ready/lib/es/es.js +++ b/src/legacy/core_plugins/console/public/np_ready/lib/es/es.js @@ -18,7 +18,6 @@ */ import { stringify as formatQueryString } from 'querystring'; - import $ from 'jquery'; const esVersion = []; diff --git a/src/legacy/core_plugins/console/public/np_ready/services/tracker.ts b/src/legacy/core_plugins/console/public/np_ready/services/tracker.ts new file mode 100644 index 0000000000000..13d5f875b3c6f --- /dev/null +++ b/src/legacy/core_plugins/console/public/np_ready/services/tracker.ts @@ -0,0 +1,31 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { METRIC_TYPE } from '@kbn/analytics'; +import { MetricsTracker } from '../types'; +import { createUiStatsReporter } from '../../../../ui_metric/public'; + +const APP_TRACKER_NAME = 'console'; +export const createUsageTracker = (): MetricsTracker => { + const track = createUiStatsReporter(APP_TRACKER_NAME); + return { + count: (eventName: string) => track(METRIC_TYPE.COUNT, eventName), + load: (eventName: string) => track(METRIC_TYPE.LOADED, eventName), + }; +}; diff --git a/src/legacy/core_plugins/console/public/np_ready/types/common.ts b/src/legacy/core_plugins/console/public/np_ready/types/common.ts index ad9ed10d4188f..e44969cd9e80a 100644 --- a/src/legacy/core_plugins/console/public/np_ready/types/common.ts +++ b/src/legacy/core_plugins/console/public/np_ready/types/common.ts @@ -17,6 +17,11 @@ * under the License. */ +export interface MetricsTracker { + count: (eventName: string) => void; + load: (eventName: string) => void; +} + export type BaseResponseType = | 'application/json' | 'text/csv' diff --git a/src/legacy/core_plugins/console/public/np_ready/types/index.ts b/src/legacy/core_plugins/console/public/np_ready/types/index.ts index 9d82237d667b3..78c6b6c8f55cc 100644 --- a/src/legacy/core_plugins/console/public/np_ready/types/index.ts +++ b/src/legacy/core_plugins/console/public/np_ready/types/index.ts @@ -20,3 +20,4 @@ export * from './core_editor'; export * from './token'; export * from './tokens_provider'; +export * from './common'; diff --git a/x-pack/legacy/plugins/console_extensions/spec/overrides/sql.query.json b/x-pack/legacy/plugins/console_extensions/spec/overrides/sql.query.json index 843fba30bb489..c78cfeea8473d 100644 --- a/x-pack/legacy/plugins/console_extensions/spec/overrides/sql.query.json +++ b/x-pack/legacy/plugins/console_extensions/spec/overrides/sql.query.json @@ -19,6 +19,7 @@ "smile" ] }, - "template": "_sql?format=json\n{\n \"query\": \"\"\"\n SELECT * FROM \"${1:TABLE}\"\n \"\"\"\n}\n" + "template": "_sql?format=json\n{\n \"query\": \"\"\"\n SELECT * FROM \"${1:TABLE}\"\n \"\"\"\n}\n", + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/sql-rest-overview.html" } } From 7607c162fe0b464052d0324580e589c5a84e4ea3 Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Tue, 7 Jan 2020 12:53:58 +0100 Subject: [PATCH 3/7] removes logout (#54098) --- .../siem/cypress/integration/lib/logout/index.ts | 15 --------------- .../cypress/integration/lib/logout/selectors.ts | 11 ----------- .../events_viewer/events_viewer.spec.ts | 5 ----- .../fields_browser/fields_browser.spec.ts | 5 ----- .../smoke_tests/inspect/inspect.spec.ts | 8 -------- .../ml_conditional_links.spec.ts | 5 ----- .../smoke_tests/navigation/navigation.spec.ts | 5 ----- .../smoke_tests/overview/overview.spec.ts | 5 ----- .../smoke_tests/pagination/pagination.spec.ts | 5 ----- .../smoke_tests/timeline/data_providers.spec.ts | 5 ----- .../smoke_tests/timeline/flyout_button.spec.ts | 5 ----- .../smoke_tests/timeline/search_or_filter.spec.ts | 5 ----- .../smoke_tests/timeline/toggle_column.spec.ts | 5 ----- .../smoke_tests/url_state/url_state.spec.ts | 5 ----- 14 files changed, 89 deletions(-) delete mode 100644 x-pack/legacy/plugins/siem/cypress/integration/lib/logout/index.ts delete mode 100644 x-pack/legacy/plugins/siem/cypress/integration/lib/logout/selectors.ts diff --git a/x-pack/legacy/plugins/siem/cypress/integration/lib/logout/index.ts b/x-pack/legacy/plugins/siem/cypress/integration/lib/logout/index.ts deleted file mode 100644 index 7a6c7f71bc98c..0000000000000 --- a/x-pack/legacy/plugins/siem/cypress/integration/lib/logout/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const logout = (): null => { - cy.request({ - method: 'GET', - url: `${Cypress.config().baseUrl}/logout`, - }).then(response => { - expect(response.status).to.eq(200); - }); - return null; -}; diff --git a/x-pack/legacy/plugins/siem/cypress/integration/lib/logout/selectors.ts b/x-pack/legacy/plugins/siem/cypress/integration/lib/logout/selectors.ts deleted file mode 100644 index 8cf015619f4c1..0000000000000 --- a/x-pack/legacy/plugins/siem/cypress/integration/lib/logout/selectors.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -/** The avatar / button that represents the logged-in Kibana user */ -export const USER_MENU = '[data-test-subj="userMenuButton"]'; - -/** Clicking this link logs out the currently logged-in Kibana user */ -export const LOGOUT_LINK = '[data-test-subj="logoutLink"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/events_viewer/events_viewer.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/events_viewer/events_viewer.spec.ts index 85878d8225609..79169d3769a78 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/events_viewer/events_viewer.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/events_viewer/events_viewer.spec.ts @@ -10,7 +10,6 @@ import { FIELDS_BROWSER_SELECTED_CATEGORY_TITLE, FIELDS_BROWSER_TITLE, } from '../../lib/fields_browser/selectors'; -import { logout } from '../../lib/logout'; import { HOSTS_PAGE } from '../../lib/urls'; import { loginAndWaitForPage, DEFAULT_TIMEOUT } from '../../lib/util/helpers'; import { @@ -46,10 +45,6 @@ describe('Events Viewer', () => { clickEventsTab(); }); - afterEach(() => { - return logout(); - }); - it('renders the fields browser with the expected title when the Events Viewer Fields Browser button is clicked', () => { openEventsViewerFieldsBrowser(); diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/fields_browser/fields_browser.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/fields_browser/fields_browser.spec.ts index dfc5e10893ebb..95df907893fc7 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/fields_browser/fields_browser.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/fields_browser/fields_browser.spec.ts @@ -22,7 +22,6 @@ import { FIELDS_BROWSER_SYSTEM_CATEGORIES_COUNT, FIELDS_BROWSER_TITLE, } from '../../lib/fields_browser/selectors'; -import { logout } from '../../lib/logout'; import { HOSTS_PAGE } from '../../lib/urls'; import { loginAndWaitForPage, DEFAULT_TIMEOUT } from '../../lib/util/helpers'; @@ -42,10 +41,6 @@ describe('Fields Browser', () => { loginAndWaitForPage(HOSTS_PAGE); }); - afterEach(() => { - return logout(); - }); - it('renders the fields browser with the expected title when the Fields button is clicked', () => { populateTimeline(); diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/inspect/inspect.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/inspect/inspect.spec.ts index 54207966fd36f..ee25705a83989 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/inspect/inspect.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/inspect/inspect.spec.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { logout } from '../../lib/logout'; import { HOSTS_PAGE } from '../../lib/urls'; import { INSPECT_BUTTON_ICON, @@ -18,9 +17,6 @@ import { executeKQL, hostExistsQuery, toggleTimelineVisibility } from '../../lib describe('Inspect', () => { describe('Hosts and network stats and tables', () => { - afterEach(() => { - return logout(); - }); INSPECT_BUTTONS_IN_SIEM.map(table => it(`inspects the ${table.title}`, () => { loginAndWaitForPage(table.url); @@ -36,10 +32,6 @@ describe('Inspect', () => { }); describe('Timeline', () => { - afterEach(() => { - return logout(); - }); - it('inspects the timeline', () => { loginAndWaitForPage(HOSTS_PAGE); toggleTimelineVisibility(); diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/ml_conditional_links/ml_conditional_links.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/ml_conditional_links/ml_conditional_links.spec.ts index 4c29c081b3e69..afeb8c3c13a4f 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/ml_conditional_links/ml_conditional_links.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/ml_conditional_links/ml_conditional_links.spec.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { logout } from '../../lib/logout'; import { mlNetworkSingleIpNullKqlQuery, mlNetworkSingleIpKqlQuery, @@ -24,10 +23,6 @@ import { loginAndWaitForPage } from '../../lib/util/helpers'; import { KQL_INPUT } from '../../lib/url_state'; describe('ml conditional links', () => { - afterEach(() => { - return logout(); - }); - it('sets the KQL from a single IP with a value for the query', () => { loginAndWaitForPage(mlNetworkSingleIpKqlQuery); cy.get(KQL_INPUT, { timeout: 5000 }).should( diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/navigation/navigation.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/navigation/navigation.spec.ts index f4beba7cbb72d..bb1a0379ce0ea 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/navigation/navigation.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/navigation/navigation.spec.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { logout } from '../../lib/logout'; import { OVERVIEW_PAGE, TIMELINES_PAGE } from '../../lib/urls'; import { NAVIGATION_HOSTS, @@ -15,10 +14,6 @@ import { import { loginAndWaitForPage } from '../../lib/util/helpers'; describe('top-level navigation common to all pages in the SIEM app', () => { - afterEach(() => { - return logout(); - }); - it('navigates to the Overview page', () => { loginAndWaitForPage(TIMELINES_PAGE); diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/overview/overview.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/overview/overview.spec.ts index 2ea8b5e8bc5ce..4ef3eb67cafc9 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/overview/overview.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/overview/overview.spec.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { logout } from '../../lib/logout'; import { OVERVIEW_PAGE } from '../../lib/urls'; import { clearFetch, stubApi } from '../../lib/fixtures/helpers'; import { HOST_STATS, NETWORK_STATS, STAT_AUDITD } from '../../lib/overview/selectors'; @@ -17,10 +16,6 @@ describe('Overview Page', () => { loginAndWaitForPage(OVERVIEW_PAGE); }); - afterEach(() => { - return logout(); - }); - it('Host and Network stats render with correct values', () => { cy.get(STAT_AUDITD.domId); diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/pagination/pagination.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/pagination/pagination.spec.ts index ebd0ad0125efb..73711f1434d5f 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/pagination/pagination.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/pagination/pagination.spec.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { logout } from '../../lib/logout'; import { HOSTS_PAGE_TAB_URLS } from '../../lib/urls'; import { AUTHENTICATIONS_TABLE, @@ -19,10 +18,6 @@ import { import { DEFAULT_TIMEOUT, loginAndWaitForPage, waitForTableLoad } from '../../lib/util/helpers'; describe('Pagination', () => { - afterEach(() => { - return logout(); - }); - it('pagination updates results and page number', () => { loginAndWaitForPage(HOSTS_PAGE_TAB_URLS.uncommonProcesses); waitForTableLoad(UNCOMMON_PROCCESSES_TABLE); diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/data_providers.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/data_providers.spec.ts index 236d5a53481b7..824e403185238 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/data_providers.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/data_providers.spec.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { logout } from '../../lib/logout'; import { TIMELINE_DATA_PROVIDERS, TIMELINE_DROPPED_DATA_PROVIDERS, @@ -22,10 +21,6 @@ describe('timeline data providers', () => { loginAndWaitForPage(HOSTS_PAGE); }); - afterEach(() => { - return logout(); - }); - it('renders the data provider of a host dragged from the All Hosts widget on the hosts page', () => { waitForAllHostsWidget(); diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/flyout_button.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/flyout_button.spec.ts index c1c35e497d081..5b0ac03ae87dc 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/flyout_button.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/flyout_button.spec.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { logout } from '../../lib/logout'; import { TIMELINE_FLYOUT_BODY, TIMELINE_NOT_READY_TO_DROP_BUTTON, @@ -21,10 +20,6 @@ describe('timeline flyout button', () => { loginAndWaitForPage(HOSTS_PAGE); }); - afterEach(() => { - return logout(); - }); - it('toggles open the timeline', () => { toggleTimelineVisibility(); diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/search_or_filter.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/search_or_filter.spec.ts index 0c9aed33d47ad..9f21b4e3d53a1 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/search_or_filter.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/search_or_filter.spec.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { logout } from '../../lib/logout'; import { assertAtLeastOneEventMatchesSearch, executeKQL, @@ -19,10 +18,6 @@ describe('timeline search or filter KQL bar', () => { loginAndWaitForPage(HOSTS_PAGE); }); - afterEach(() => { - return logout(); - }); - it('executes a KQL query', () => { toggleTimelineVisibility(); diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/toggle_column.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/toggle_column.spec.ts index 8197f77db9a08..9a915b0e77d44 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/toggle_column.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/toggle_column.spec.ts @@ -6,7 +6,6 @@ import { drag, drop } from '../../lib/drag_n_drop/helpers'; import { populateTimeline } from '../../lib/fields_browser/helpers'; -import { logout } from '../../lib/logout'; import { toggleFirstTimelineEventDetails } from '../../lib/timeline/helpers'; import { HOSTS_PAGE } from '../../lib/urls'; import { loginAndWaitForPage, DEFAULT_TIMEOUT } from '../../lib/util/helpers'; @@ -16,10 +15,6 @@ describe('toggle column in timeline', () => { loginAndWaitForPage(HOSTS_PAGE); }); - afterEach(() => { - return logout(); - }); - const timestampField = '@timestamp'; const idField = '_id'; diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/url_state/url_state.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/url_state/url_state.spec.ts index dba5099a93c5a..33ee2cb1cb302 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/url_state/url_state.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/url_state/url_state.spec.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { logout } from '../../lib/logout'; import { ABSOLUTE_DATE_RANGE, DATE_PICKER_ABSOLUTE_INPUT, @@ -33,10 +32,6 @@ import { waitForAllHostsWidget } from '../../lib/hosts/helpers'; import { NAVIGATION_HOSTS_ALL_HOSTS, NAVIGATION_HOSTS_ANOMALIES } from '../../lib/hosts/selectors'; describe('url state', () => { - afterEach(() => { - return logout(); - }); - it('sets the global start and end dates from the url', () => { loginAndWaitForPage(ABSOLUTE_DATE_RANGE.url); cy.get(DATE_PICKER_START_DATE_POPOVER_BUTTON).should( From 58cb24a7e66f5e740e53ad5a16bb2ced4a98ca2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Tue, 7 Jan 2020 11:56:23 +0000 Subject: [PATCH 4/7] [APM] Show errors on the timeline instead of under the transaction (#53756) * creating error marker and refactoring some stuff * styling popover * adding agent marks and errors to waterfall items * adding agent marks and errors to waterfall items * adding agent marks and errors to waterfall items * fixing tests and typescript checking * refactoring helper * changing transaction error badge style * adding unit test * fixing agent marker position * fixing offset when error is registered before its parent * refactoring error marker * refactoring error marker * refactoring error marker * refactoring error marker * refactoring error marker * refactoring waterfall helper * refactoring waterfall helper * refactoring waterfall helper api * refactoring waterfall helper * removing unused code * refactoring waterfall helper * changing unit test * removing comment * refactoring marker component and waterfall helper * removing servicecolor from waterfall item and adding it to errormark * fixing trace order --- .../WaterfallWithSummmary/ErrorCount.tsx | 32 + .../WaterfallWithSummmary/ErrorCountBadge.tsx | 17 - .../MaybeViewTraceLink.tsx | 17 +- .../WaterfallWithSummmary/TransactionTabs.tsx | 1 - .../__test__}/get_agent_marks.test.ts | 25 +- .../Marks/__test__/get_error_marks.test.ts | 97 + .../Marks/get_agent_marks.ts | 31 + .../Marks/get_error_marks.ts | 39 + .../WaterfallContainer/Marks/index.ts | 12 + .../WaterfallContainer/ServiceLegends.tsx | 2 +- .../Waterfall/SpanFlyout/index.tsx | 2 +- .../Waterfall/TransactionFlyout/index.tsx | 10 +- .../Waterfall/WaterfallFlyout.tsx | 59 + .../Waterfall/WaterfallItem.tsx | 83 +- .../WaterfallContainer/Waterfall/index.tsx | 216 +- .../waterfall_helpers.test.ts.snap | 1771 +++++++++++------ .../waterfall_helpers.test.ts | 241 ++- .../waterfall_helpers/waterfall_helpers.ts | 254 ++- .../WaterfallContainer/get_agent_marks.ts | 27 - .../WaterfallContainer/index.tsx | 9 +- .../__tests__/ErrorCount.test.tsx | 37 + .../WaterfallWithSummmary/index.tsx | 27 +- .../components/shared/Links/apm/APMLink.tsx | 2 +- ...tem.tsx => ErrorCountSummaryItemBadge.tsx} | 27 +- .../shared/Summary/TransactionSummary.tsx | 4 +- .../ErrorCountSummaryItemBadge.test.tsx | 21 + .../shared/charts/CustomPlot/Legends.js | 2 +- .../__snapshots__/CustomPlot.test.js.snap | 27 + .../components/shared/charts/Legend/index.js | 56 - .../components/shared/charts/Legend/index.tsx | 94 + .../AgentMarker.tsx} | 40 +- .../charts/Timeline/Marker/ErrorMarker.tsx | 103 + .../Marker/__test__/AgentMarker.test.tsx | 23 + .../Marker/__test__/ErrorMarker.test.tsx | 30 + .../Timeline/Marker/__test__/Marker.test.tsx | 42 + .../__snapshots__/AgentMarker.test.tsx.snap | 26 + .../__snapshots__/ErrorMarker.test.tsx.snap | 49 + .../__snapshots__/Marker.test.tsx.snap | 58 + .../shared/charts/Timeline/Marker/index.tsx | 36 + .../shared/charts/Timeline/TimelineAxis.js | 16 +- .../shared/charts/Timeline/VerticalLines.js | 12 +- .../charts/Timeline/__test__/Timeline.test.js | 23 +- .../__snapshots__/Timeline.test.js.snap | 197 +- .../shared/charts/Timeline/index.js | 8 +- .../components/shared/charts/Tooltip/index.js | 2 +- .../get_trace_errors_per_transaction.ts | 1 - .../traces/__snapshots__/queries.test.ts.snap | 1 + .../apm/server/lib/traces/get_trace_items.ts | 5 +- 48 files changed, 2575 insertions(+), 1339 deletions(-) create mode 100644 x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/ErrorCount.tsx delete mode 100644 x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/ErrorCountBadge.tsx rename x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/{ => Marks/__test__}/get_agent_marks.test.ts (66%) create mode 100644 x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_error_marks.test.ts create mode 100644 x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.ts create mode 100644 x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.ts create mode 100644 x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/index.ts create mode 100644 x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallFlyout.tsx delete mode 100644 x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/get_agent_marks.ts create mode 100644 x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/__tests__/ErrorCount.test.tsx rename x-pack/legacy/plugins/apm/public/components/shared/Summary/{ErrorCountSummaryItem.tsx => ErrorCountSummaryItemBadge.tsx} (51%) create mode 100644 x-pack/legacy/plugins/apm/public/components/shared/Summary/__test__/ErrorCountSummaryItemBadge.test.tsx delete mode 100644 x-pack/legacy/plugins/apm/public/components/shared/charts/Legend/index.js create mode 100644 x-pack/legacy/plugins/apm/public/components/shared/charts/Legend/index.tsx rename x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/{AgentMarker.js => Marker/AgentMarker.tsx} (56%) create mode 100644 x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx create mode 100644 x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/AgentMarker.test.tsx create mode 100644 x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/ErrorMarker.test.tsx create mode 100644 x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/Marker.test.tsx create mode 100644 x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/__snapshots__/AgentMarker.test.tsx.snap create mode 100644 x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/__snapshots__/ErrorMarker.test.tsx.snap create mode 100644 x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/__snapshots__/Marker.test.tsx.snap create mode 100644 x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/ErrorCount.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/ErrorCount.tsx new file mode 100644 index 0000000000000..ff2cb69d011fa --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/ErrorCount.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiText, EuiTextColor } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; + +interface Props { + count: number; +} + +export const ErrorCount = ({ count }: Props) => ( + +

+ { + e.stopPropagation(); + }} + > + {i18n.translate('xpack.apm.transactionDetails.errorCount', { + defaultMessage: + '{errorCount, number} {errorCount, plural, one {Error} other {Errors}}', + values: { errorCount: count } + })} + +

+
+); diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/ErrorCountBadge.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/ErrorCountBadge.tsx deleted file mode 100644 index 4c3ec3ca9f308..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/ErrorCountBadge.tsx +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiBadge } from '@elastic/eui'; -import euiThemeLight from '@elastic/eui/dist/eui_theme_light.json'; -import React from 'react'; - -type Props = React.ComponentProps; - -export const ErrorCountBadge = ({ children, ...rest }: Props) => ( - - {children} - -); diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/MaybeViewTraceLink.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/MaybeViewTraceLink.tsx index 39e52be34a415..322ec7c422571 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/MaybeViewTraceLink.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/MaybeViewTraceLink.tsx @@ -25,8 +25,9 @@ export const MaybeViewTraceLink = ({ } ); + const { rootTransaction } = waterfall; // the traceroot cannot be found, so we cannot link to it - if (!waterfall.traceRoot) { + if (!rootTransaction) { return ( {viewFullTraceButtonLabel} diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/TransactionTabs.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/TransactionTabs.tsx index e5be12509e3c9..f8318b9ae97e6 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/TransactionTabs.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/TransactionTabs.tsx @@ -77,7 +77,6 @@ export function TransactionTabs({ {currentTab.key === timelineTab.key ? ( { it('should sort the marks by time', () => { @@ -21,9 +21,24 @@ describe('getAgentMarks', () => { } } as any; expect(getAgentMarks(transaction)).toEqual([ - { name: 'timeToFirstByte', us: 10000 }, - { name: 'domInteractive', us: 117000 }, - { name: 'domComplete', us: 118000 } + { + id: 'timeToFirstByte', + offset: 10000, + type: 'agentMark', + verticalLine: true + }, + { + id: 'domInteractive', + offset: 117000, + type: 'agentMark', + verticalLine: true + }, + { + id: 'domComplete', + offset: 118000, + type: 'agentMark', + verticalLine: true + } ]); }); diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_error_marks.test.ts b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_error_marks.test.ts new file mode 100644 index 0000000000000..8fd8edd7f8a72 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_error_marks.test.ts @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IWaterfallItem } from '../../Waterfall/waterfall_helpers/waterfall_helpers'; +import { getErrorMarks } from '../get_error_marks'; + +describe('getErrorMarks', () => { + describe('returns empty array', () => { + it('when items are missing', () => { + expect(getErrorMarks([], {})).toEqual([]); + }); + it('when any error is available', () => { + const items = [ + { docType: 'span' }, + { docType: 'transaction' } + ] as IWaterfallItem[]; + expect(getErrorMarks(items, {})).toEqual([]); + }); + }); + + it('returns error marks', () => { + const items = [ + { + docType: 'error', + offset: 10, + skew: 5, + doc: { error: { id: 1 }, service: { name: 'opbeans-java' } } + } as unknown, + { docType: 'transaction' }, + { + docType: 'error', + offset: 50, + skew: 0, + doc: { error: { id: 2 }, service: { name: 'opbeans-node' } } + } as unknown + ] as IWaterfallItem[]; + expect( + getErrorMarks(items, { 'opbeans-java': 'red', 'opbeans-node': 'blue' }) + ).toEqual([ + { + type: 'errorMark', + offset: 15, + verticalLine: false, + id: 1, + error: { error: { id: 1 }, service: { name: 'opbeans-java' } }, + serviceColor: 'red' + }, + { + type: 'errorMark', + offset: 50, + verticalLine: false, + id: 2, + error: { error: { id: 2 }, service: { name: 'opbeans-node' } }, + serviceColor: 'blue' + } + ]); + }); + + it('returns error marks without service color', () => { + const items = [ + { + docType: 'error', + offset: 10, + skew: 5, + doc: { error: { id: 1 }, service: { name: 'opbeans-java' } } + } as unknown, + { docType: 'transaction' }, + { + docType: 'error', + offset: 50, + skew: 0, + doc: { error: { id: 2 }, service: { name: 'opbeans-node' } } + } as unknown + ] as IWaterfallItem[]; + expect(getErrorMarks(items, {})).toEqual([ + { + type: 'errorMark', + offset: 15, + verticalLine: false, + id: 1, + error: { error: { id: 1 }, service: { name: 'opbeans-java' } }, + serviceColor: undefined + }, + { + type: 'errorMark', + offset: 50, + verticalLine: false, + id: 2, + error: { error: { id: 2 }, service: { name: 'opbeans-node' } }, + serviceColor: undefined + } + ]); + }); +}); diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.ts b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.ts new file mode 100644 index 0000000000000..7798d716cb219 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { sortBy } from 'lodash'; +import { Transaction } from '../../../../../../../typings/es_schemas/ui/Transaction'; +import { Mark } from '.'; + +// Extends Mark without adding new properties to it. +export interface AgentMark extends Mark { + type: 'agentMark'; +} + +export function getAgentMarks(transaction?: Transaction): AgentMark[] { + const agent = transaction?.transaction.marks?.agent; + if (!agent) { + return []; + } + + return sortBy( + Object.entries(agent).map(([name, ms]) => ({ + type: 'agentMark', + id: name, + offset: ms * 1000, + verticalLine: true + })), + 'offset' + ); +} diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.ts b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.ts new file mode 100644 index 0000000000000..f1f0163a49d10 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { isEmpty } from 'lodash'; +import { ErrorRaw } from '../../../../../../../typings/es_schemas/raw/ErrorRaw'; +import { + IWaterfallItem, + IWaterfallError, + IServiceColors +} from '../Waterfall/waterfall_helpers/waterfall_helpers'; +import { Mark } from '.'; + +export interface ErrorMark extends Mark { + type: 'errorMark'; + error: ErrorRaw; + serviceColor?: string; +} + +export const getErrorMarks = ( + items: IWaterfallItem[], + serviceColors: IServiceColors +): ErrorMark[] => { + if (isEmpty(items)) { + return []; + } + + return (items.filter( + item => item.docType === 'error' + ) as IWaterfallError[]).map(error => ({ + type: 'errorMark', + offset: error.offset + error.skew, + verticalLine: false, + id: error.doc.error.id, + error: error.doc, + serviceColor: serviceColors[error.doc.service.name] + })); +}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/index.ts b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/index.ts new file mode 100644 index 0000000000000..52f811f5c3969 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface Mark { + type: string; + offset: number; + verticalLine: boolean; + id: string; +} diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/ServiceLegends.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/ServiceLegends.tsx index e4cb4ff62b36c..4e6a0eaf45585 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/ServiceLegends.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/ServiceLegends.tsx @@ -10,7 +10,7 @@ import React from 'react'; import styled from 'styled-components'; import { px, unit } from '../../../../../style/variables'; // @ts-ignore -import Legend from '../../../../shared/charts/Legend'; +import { Legend } from '../../../../shared/charts/Legend'; import { IServiceColors } from './Waterfall/waterfall_helpers/waterfall_helpers'; const Legends = styled.div` diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx index cc1f9dd529bce..4863d6519de07 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx @@ -101,7 +101,7 @@ export function SpanFlyout({ const dbContext = span.span.db; const httpContext = span.span.http; const spanTypes = getSpanTypes(span); - const spanHttpStatusCode = httpContext?.response.status_code; + const spanHttpStatusCode = httpContext?.response?.status_code; const spanHttpUrl = httpContext?.url?.original; const spanHttpMethod = httpContext?.method; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/index.tsx index 2020b8252035b..df95577c81eff 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/index.tsx @@ -27,8 +27,8 @@ import { DroppedSpansWarning } from './DroppedSpansWarning'; interface Props { onClose: () => void; transaction?: Transaction; - errorCount: number; - traceRootDuration?: number; + errorCount?: number; + rootTransactionDuration?: number; } function TransactionPropertiesTable({ @@ -49,8 +49,8 @@ function TransactionPropertiesTable({ export function TransactionFlyout({ transaction: transactionDoc, onClose, - errorCount, - traceRootDuration + errorCount = 0, + rootTransactionDuration }: Props) { if (!transactionDoc) { return null; @@ -84,7 +84,7 @@ export function TransactionFlyout({ diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallFlyout.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallFlyout.tsx new file mode 100644 index 0000000000000..426088f0bb36a --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallFlyout.tsx @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Location } from 'history'; +import React from 'react'; +import { SpanFlyout } from './SpanFlyout'; +import { TransactionFlyout } from './TransactionFlyout'; +import { IWaterfall } from './waterfall_helpers/waterfall_helpers'; + +interface Props { + waterfallItemId?: string; + waterfall: IWaterfall; + location: Location; + toggleFlyout: ({ location }: { location: Location }) => void; +} +export const WaterfallFlyout: React.FC = ({ + waterfallItemId, + waterfall, + location, + toggleFlyout +}) => { + const currentItem = waterfall.items.find(item => item.id === waterfallItemId); + + if (!currentItem) { + return null; + } + + switch (currentItem.docType) { + case 'span': + const parentTransaction = + currentItem.parent?.docType === 'transaction' + ? currentItem.parent?.doc + : undefined; + + return ( + toggleFlyout({ location })} + /> + ); + case 'transaction': + return ( + toggleFlyout({ location })} + rootTransactionDuration={ + waterfall.rootTransaction?.transaction.duration.us + } + errorCount={waterfall.errorsPerTransaction[currentItem.id]} + /> + ); + default: + return null; + } +}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx index 8d4fab4aa8dd9..8a82547d717db 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx @@ -13,12 +13,12 @@ import { i18n } from '@kbn/i18n'; import { isRumAgentName } from '../../../../../../../common/agent_name'; import { px, unit, units } from '../../../../../../style/variables'; import { asDuration } from '../../../../../../utils/formatters'; -import { ErrorCountBadge } from '../../ErrorCountBadge'; +import { ErrorCount } from '../../ErrorCount'; import { IWaterfallItem } from './waterfall_helpers/waterfall_helpers'; import { ErrorOverviewLink } from '../../../../../shared/Links/apm/ErrorOverviewLink'; import { TRACE_ID } from '../../../../../../../common/elasticsearch_fieldnames'; -type ItemType = 'transaction' | 'span'; +type ItemType = 'transaction' | 'span' | 'error'; interface IContainerStyleProps { type: ItemType; @@ -89,24 +89,29 @@ interface IWaterfallItemProps { } function PrefixIcon({ item }: { item: IWaterfallItem }) { - if (item.docType === 'span') { - // icon for database spans - const isDbType = item.span.span.type.startsWith('db'); - if (isDbType) { - return ; + switch (item.docType) { + case 'span': { + // icon for database spans + const isDbType = item.doc.span.type.startsWith('db'); + if (isDbType) { + return ; + } + + // omit icon for other spans + return null; } - - // omit icon for other spans - return null; - } - - // icon for RUM agent transactions - if (isRumAgentName(item.transaction.agent.name)) { - return ; + case 'transaction': { + // icon for RUM agent transactions + if (isRumAgentName(item.doc.agent.name)) { + return ; + } + + // icon for other transactions + return ; + } + default: + return null; } - - // icon for other transactions - return ; } interface SpanActionToolTipProps { @@ -117,11 +122,9 @@ const SpanActionToolTip: React.FC = ({ item, children }) => { - if (item && item.docType === 'span') { + if (item?.docType === 'span') { return ( - + <>{children} ); @@ -140,9 +143,8 @@ function Duration({ item }: { item: IWaterfallItem }) { function HttpStatusCode({ item }: { item: IWaterfallItem }) { // http status code for transactions of type 'request' const httpStatusCode = - item.docType === 'transaction' && - item.transaction.transaction.type === 'request' - ? item.transaction.transaction.result + item.docType === 'transaction' && item.doc.transaction.type === 'request' + ? item.doc.transaction.result : undefined; if (!httpStatusCode) { @@ -153,14 +155,18 @@ function HttpStatusCode({ item }: { item: IWaterfallItem }) { } function NameLabel({ item }: { item: IWaterfallItem }) { - if (item.docType === 'span') { - return {item.name}; + switch (item.docType) { + case 'span': + return {item.doc.span.name}; + case 'transaction': + return ( + +
{item.doc.transaction.name}
+
+ ); + default: + return null; } - return ( - -
{item.name}
-
- ); } export function WaterfallItem({ @@ -210,24 +216,17 @@ export function WaterfallItem({ {errorCount > 0 && item.docType === 'transaction' ? ( - { - event.stopPropagation(); - }} - onClickAriaLabel={tooltipContent} - > - {errorCount} - + ) : null} diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx index d53b4077d9759..b48fc1cf7ca27 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx @@ -4,31 +4,26 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiCallOut } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { Location } from 'history'; -import React, { Component } from 'react'; +import React from 'react'; // @ts-ignore import { StickyContainer } from 'react-sticky'; import styled from 'styled-components'; -import { EuiCallOut } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { IUrlParams } from '../../../../../../context/UrlParamsContext/types'; +import { px } from '../../../../../../style/variables'; +import { history } from '../../../../../../utils/history'; // @ts-ignore import Timeline from '../../../../../shared/charts/Timeline'; +import { fromQuery, toQuery } from '../../../../../shared/Links/url_helpers'; +import { getAgentMarks } from '../Marks/get_agent_marks'; +import { getErrorMarks } from '../Marks/get_error_marks'; +import { WaterfallFlyout } from './WaterfallFlyout'; +import { WaterfallItem } from './WaterfallItem'; import { - APMQueryParams, - fromQuery, - toQuery -} from '../../../../../shared/Links/url_helpers'; -import { history } from '../../../../../../utils/history'; -import { AgentMark } from '../get_agent_marks'; -import { SpanFlyout } from './SpanFlyout'; -import { TransactionFlyout } from './TransactionFlyout'; -import { - IServiceColors, IWaterfall, IWaterfallItem } from './waterfall_helpers/waterfall_helpers'; -import { WaterfallItem } from './WaterfallItem'; const Container = styled.div` transition: 0.1s padding ease; @@ -43,138 +38,105 @@ const TIMELINE_MARGINS = { bottom: 0 }; +const toggleFlyout = ({ + item, + location +}: { + item?: IWaterfallItem; + location: Location; +}) => { + history.replace({ + ...location, + search: fromQuery({ + ...toQuery(location.search), + flyoutDetailTab: undefined, + waterfallItemId: item?.id + }) + }); +}; + +const WaterfallItemsContainer = styled.div<{ + paddingTop: number; +}>` + padding-top: ${props => px(props.paddingTop)}; +`; + interface Props { - agentMarks: AgentMark[]; - urlParams: IUrlParams; + waterfallItemId?: string; waterfall: IWaterfall; location: Location; - serviceColors: IServiceColors; exceedsMax: boolean; } -export class Waterfall extends Component { - public onOpenFlyout = (item: IWaterfallItem) => { - this.setQueryParams({ - flyoutDetailTab: undefined, - waterfallItemId: String(item.id) - }); - }; +export const Waterfall: React.FC = ({ + waterfall, + exceedsMax, + waterfallItemId, + location +}) => { + const itemContainerHeight = 58; // TODO: This is a nasty way to calculate the height of the svg element. A better approach should be found + const waterfallHeight = itemContainerHeight * waterfall.items.length; - public onCloseFlyout = () => { - this.setQueryParams({ - flyoutDetailTab: undefined, - waterfallItemId: undefined - }); - }; + const { serviceColors, duration } = waterfall; - public renderWaterfallItem = (item: IWaterfallItem) => { - const { serviceColors, waterfall, urlParams }: Props = this.props; + const agentMarks = getAgentMarks(waterfall.entryTransaction); + const errorMarks = getErrorMarks(waterfall.items, serviceColors); + + const renderWaterfallItem = (item: IWaterfallItem) => { + if (item.docType === 'error') { + return null; + } const errorCount = item.docType === 'transaction' - ? waterfall.errorCountByTransactionId[item.transaction.transaction.id] + ? waterfall.errorsPerTransaction[item.doc.transaction.id] : 0; return ( this.onOpenFlyout(item)} + onClick={() => toggleFlyout({ item, location })} /> ); }; - public getFlyOut = () => { - const { waterfall, urlParams } = this.props; - - const currentItem = - urlParams.waterfallItemId && - waterfall.itemsById[urlParams.waterfallItemId]; - - if (!currentItem) { - return null; - } - - switch (currentItem.docType) { - case 'span': - const parentTransaction = waterfall.getTransactionById( - currentItem.parentId - ); - - return ( - - ); - case 'transaction': - return ( - - ); - default: - return null; - } - }; - - public render() { - const { waterfall, exceedsMax } = this.props; - const itemContainerHeight = 58; // TODO: This is a nasty way to calculate the height of the svg element. A better approach should be found - const waterfallHeight = itemContainerHeight * waterfall.orderedItems.length; - - return ( - - {exceedsMax ? ( - - ) : null} - - -
- {waterfall.orderedItems.map(this.renderWaterfallItem)} -
-
- - {this.getFlyOut()} -
- ); - } - - private setQueryParams(params: APMQueryParams) { - const { location } = this.props; - history.replace({ - ...location, - search: fromQuery({ - ...toQuery(location.search), - ...params - }) - }); - } -} + return ( + + {exceedsMax && ( + + )} + + + + {waterfall.items.map(renderWaterfallItem)} + + + + + + ); +}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap index 6f61f62167638..ece396bc4cfc4 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap @@ -24,145 +24,44 @@ Object { "name": "GET /api", }, }, - "errorCountByTransactionId": Object { + "errorsCount": 1, + "errorsPerTransaction": Object { "myTransactionId1": 2, "myTransactionId2": 3, }, - "getTransactionById": [Function], - "itemsById": Object { - "mySpanIdA": Object { - "childIds": Array [ - "mySpanIdB", - "mySpanIdC", - ], - "docType": "span", - "duration": 6161, - "id": "mySpanIdA", - "name": "Api::ProductsController#index", - "offset": 40498, - "parentId": "myTransactionId2", - "serviceName": "opbeans-ruby", - "skew": 0, - "span": Object { - "parent": Object { - "id": "myTransactionId2", - }, + "items": Array [ + Object { + "doc": Object { "processor": Object { - "event": "span", + "event": "transaction", }, "service": Object { - "name": "opbeans-ruby", - }, - "span": Object { - "duration": Object { - "us": 6161, - }, - "id": "mySpanIdA", - "name": "Api::ProductsController#index", + "name": "opbeans-node", }, "timestamp": Object { - "us": 1549324795824504, + "us": 1549324795784006, }, "trace": Object { "id": "myTraceId", }, "transaction": Object { - "id": "myTransactionId2", - }, - }, - "timestamp": 1549324795824504, - }, - "mySpanIdB": Object { - "childIds": Array [], - "docType": "span", - "duration": 481, - "id": "mySpanIdB", - "name": "SELECT FROM products", - "offset": 41627, - "parentId": "mySpanIdA", - "serviceName": "opbeans-ruby", - "skew": 0, - "span": Object { - "parent": Object { - "id": "mySpanIdA", - }, - "processor": Object { - "event": "span", - }, - "service": Object { - "name": "opbeans-ruby", - }, - "span": Object { "duration": Object { - "us": 481, + "us": 49660, }, - "id": "mySpanIdB", - "name": "SELECT FROM products", - }, - "timestamp": Object { - "us": 1549324795825633, - }, - "trace": Object { - "id": "myTraceId", - }, - "transaction": Object { - "id": "myTransactionId2", + "id": "myTransactionId1", + "name": "GET /api", }, }, - "timestamp": 1549324795825633, - }, - "mySpanIdC": Object { - "childIds": Array [], - "docType": "span", - "duration": 532, - "id": "mySpanIdC", - "name": "SELECT FROM product", - "offset": 43899, - "parentId": "mySpanIdA", - "serviceName": "opbeans-ruby", + "docType": "transaction", + "duration": 49660, + "id": "myTransactionId1", + "offset": 0, + "parent": undefined, + "parentId": undefined, "skew": 0, - "span": Object { - "parent": Object { - "id": "mySpanIdA", - }, - "processor": Object { - "event": "span", - }, - "service": Object { - "name": "opbeans-ruby", - }, - "span": Object { - "duration": Object { - "us": 532, - }, - "id": "mySpanIdC", - "name": "SELECT FROM product", - }, - "timestamp": Object { - "us": 1549324795827905, - }, - "trace": Object { - "id": "myTraceId", - }, - "transaction": Object { - "id": "myTransactionId2", - }, - }, - "timestamp": 1549324795827905, }, - "mySpanIdD": Object { - "childIds": Array [ - "myTransactionId2", - ], - "docType": "span", - "duration": 47557, - "id": "mySpanIdD", - "name": "GET opbeans-ruby:3000/api/products", - "offset": 1754, - "parentId": "myTransactionId1", - "serviceName": "opbeans-node", - "skew": 0, - "span": Object { + Object { + "doc": Object { "parent": Object { "id": "myTransactionId1", }, @@ -189,59 +88,45 @@ Object { "id": "myTransactionId1", }, }, - "timestamp": 1549324795785760, - }, - "myTransactionId1": Object { - "childIds": Array [ - "mySpanIdD", - ], - "docType": "transaction", - "duration": 49660, - "errorCount": 2, - "id": "myTransactionId1", - "name": "GET /api", - "offset": 0, - "parentId": undefined, - "serviceName": "opbeans-node", - "skew": 0, - "timestamp": 1549324795784006, - "transaction": Object { - "processor": Object { - "event": "transaction", - }, - "service": Object { - "name": "opbeans-node", - }, - "timestamp": Object { - "us": 1549324795784006, - }, - "trace": Object { - "id": "myTraceId", - }, - "transaction": Object { - "duration": Object { - "us": 49660, + "docType": "span", + "duration": 47557, + "id": "mySpanIdD", + "offset": 1754, + "parent": Object { + "doc": Object { + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-node", + }, + "timestamp": Object { + "us": 1549324795784006, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 49660, + }, + "id": "myTransactionId1", + "name": "GET /api", }, - "id": "myTransactionId1", - "name": "GET /api", }, + "docType": "transaction", + "duration": 49660, + "id": "myTransactionId1", + "offset": 0, + "parent": undefined, + "parentId": undefined, + "skew": 0, }, - }, - "myTransactionId2": Object { - "childIds": Array [ - "mySpanIdA", - ], - "docType": "transaction", - "duration": 8634, - "errorCount": 3, - "id": "myTransactionId2", - "name": "Api::ProductsController#index", - "offset": 39298, - "parentId": "mySpanIdD", - "serviceName": "opbeans-ruby", + "parentId": "myTransactionId1", "skew": 0, - "timestamp": 1549324795823304, - "transaction": Object { + }, + Object { + "doc": Object { "parent": Object { "id": "mySpanIdD", }, @@ -262,181 +147,403 @@ Object { "us": 8634, }, "id": "myTransactionId2", + "marks": Object { + "agent": Object { + "domComplete": 383, + "domInteractive": 382, + "timeToFirstByte": 14, + }, + }, "name": "Api::ProductsController#index", }, }, - }, - }, - "orderedItems": Array [ - Object { - "childIds": Array [ - "mySpanIdD", - ], "docType": "transaction", - "duration": 49660, - "errorCount": 2, - "id": "myTransactionId1", - "name": "GET /api", - "offset": 0, - "parentId": undefined, - "serviceName": "opbeans-node", - "skew": 0, - "timestamp": 1549324795784006, - "transaction": Object { - "processor": Object { - "event": "transaction", - }, - "service": Object { - "name": "opbeans-node", - }, - "timestamp": Object { - "us": 1549324795784006, - }, - "trace": Object { - "id": "myTraceId", + "duration": 8634, + "id": "myTransactionId2", + "offset": 39298, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId1", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-node", + }, + "span": Object { + "duration": Object { + "us": 47557, + }, + "id": "mySpanIdD", + "name": "GET opbeans-ruby:3000/api/products", + }, + "timestamp": Object { + "us": 1549324795785760, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId1", + }, }, - "transaction": Object { - "duration": Object { - "us": 49660, + "docType": "span", + "duration": 47557, + "id": "mySpanIdD", + "offset": 1754, + "parent": Object { + "doc": Object { + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-node", + }, + "timestamp": Object { + "us": 1549324795784006, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 49660, + }, + "id": "myTransactionId1", + "name": "GET /api", + }, }, + "docType": "transaction", + "duration": 49660, "id": "myTransactionId1", - "name": "GET /api", + "offset": 0, + "parent": undefined, + "parentId": undefined, + "skew": 0, }, + "parentId": "myTransactionId1", + "skew": 0, }, + "parentId": "mySpanIdD", + "skew": 0, }, Object { - "childIds": Array [ - "myTransactionId2", - ], - "docType": "span", - "duration": 47557, - "id": "mySpanIdD", - "name": "GET opbeans-ruby:3000/api/products", - "offset": 1754, - "parentId": "myTransactionId1", - "serviceName": "opbeans-node", - "skew": 0, - "span": Object { + "doc": Object { "parent": Object { - "id": "myTransactionId1", + "id": "myTransactionId2", }, "processor": Object { "event": "span", }, "service": Object { - "name": "opbeans-node", + "name": "opbeans-ruby", }, "span": Object { "duration": Object { - "us": 47557, + "us": 6161, }, - "id": "mySpanIdD", - "name": "GET opbeans-ruby:3000/api/products", + "id": "mySpanIdA", + "name": "Api::ProductsController#index", }, "timestamp": Object { - "us": 1549324795785760, + "us": 1549324795824504, }, "trace": Object { "id": "myTraceId", }, "transaction": Object { - "id": "myTransactionId1", + "id": "myTransactionId2", + }, + }, + "docType": "span", + "duration": 6161, + "id": "mySpanIdA", + "offset": 40498, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "mySpanIdD", + }, + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "timestamp": Object { + "us": 1549324795823304, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 8634, + }, + "id": "myTransactionId2", + "marks": Object { + "agent": Object { + "domComplete": 383, + "domInteractive": 382, + "timeToFirstByte": 14, + }, + }, + "name": "Api::ProductsController#index", + }, }, + "docType": "transaction", + "duration": 8634, + "id": "myTransactionId2", + "offset": 39298, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId1", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-node", + }, + "span": Object { + "duration": Object { + "us": 47557, + }, + "id": "mySpanIdD", + "name": "GET opbeans-ruby:3000/api/products", + }, + "timestamp": Object { + "us": 1549324795785760, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId1", + }, + }, + "docType": "span", + "duration": 47557, + "id": "mySpanIdD", + "offset": 1754, + "parent": Object { + "doc": Object { + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-node", + }, + "timestamp": Object { + "us": 1549324795784006, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 49660, + }, + "id": "myTransactionId1", + "name": "GET /api", + }, + }, + "docType": "transaction", + "duration": 49660, + "id": "myTransactionId1", + "offset": 0, + "parent": undefined, + "parentId": undefined, + "skew": 0, + }, + "parentId": "myTransactionId1", + "skew": 0, + }, + "parentId": "mySpanIdD", + "skew": 0, }, - "timestamp": 1549324795785760, + "parentId": "myTransactionId2", + "skew": 0, }, Object { - "childIds": Array [ - "mySpanIdA", - ], - "docType": "transaction", - "duration": 8634, - "errorCount": 3, - "id": "myTransactionId2", - "name": "Api::ProductsController#index", - "offset": 39298, - "parentId": "mySpanIdD", - "serviceName": "opbeans-ruby", - "skew": 0, - "timestamp": 1549324795823304, - "transaction": Object { + "doc": Object { "parent": Object { - "id": "mySpanIdD", + "id": "mySpanIdA", }, "processor": Object { - "event": "transaction", + "event": "span", }, "service": Object { "name": "opbeans-ruby", }, + "span": Object { + "duration": Object { + "us": 481, + }, + "id": "mySpanIdB", + "name": "SELECT FROM products", + }, "timestamp": Object { - "us": 1549324795823304, + "us": 1549324795825633, }, "trace": Object { "id": "myTraceId", }, "transaction": Object { - "duration": Object { - "us": 8634, - }, "id": "myTransactionId2", - "name": "Api::ProductsController#index", }, }, - }, - Object { - "childIds": Array [ - "mySpanIdB", - "mySpanIdC", - ], "docType": "span", - "duration": 6161, - "id": "mySpanIdA", - "name": "Api::ProductsController#index", - "offset": 40498, - "parentId": "myTransactionId2", - "serviceName": "opbeans-ruby", - "skew": 0, - "span": Object { - "parent": Object { - "id": "myTransactionId2", - }, - "processor": Object { - "event": "span", - }, - "service": Object { - "name": "opbeans-ruby", - }, - "span": Object { - "duration": Object { - "us": 6161, + "duration": 481, + "id": "mySpanIdB", + "offset": 41627, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId2", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "span": Object { + "duration": Object { + "us": 6161, + }, + "id": "mySpanIdA", + "name": "Api::ProductsController#index", + }, + "timestamp": Object { + "us": 1549324795824504, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId2", }, - "id": "mySpanIdA", - "name": "Api::ProductsController#index", - }, - "timestamp": Object { - "us": 1549324795824504, - }, - "trace": Object { - "id": "myTraceId", }, - "transaction": Object { + "docType": "span", + "duration": 6161, + "id": "mySpanIdA", + "offset": 40498, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "mySpanIdD", + }, + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "timestamp": Object { + "us": 1549324795823304, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 8634, + }, + "id": "myTransactionId2", + "marks": Object { + "agent": Object { + "domComplete": 383, + "domInteractive": 382, + "timeToFirstByte": 14, + }, + }, + "name": "Api::ProductsController#index", + }, + }, + "docType": "transaction", + "duration": 8634, "id": "myTransactionId2", + "offset": 39298, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId1", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-node", + }, + "span": Object { + "duration": Object { + "us": 47557, + }, + "id": "mySpanIdD", + "name": "GET opbeans-ruby:3000/api/products", + }, + "timestamp": Object { + "us": 1549324795785760, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId1", + }, + }, + "docType": "span", + "duration": 47557, + "id": "mySpanIdD", + "offset": 1754, + "parent": Object { + "doc": Object { + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-node", + }, + "timestamp": Object { + "us": 1549324795784006, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 49660, + }, + "id": "myTransactionId1", + "name": "GET /api", + }, + }, + "docType": "transaction", + "duration": 49660, + "id": "myTransactionId1", + "offset": 0, + "parent": undefined, + "parentId": undefined, + "skew": 0, + }, + "parentId": "myTransactionId1", + "skew": 0, + }, + "parentId": "mySpanIdD", + "skew": 0, }, + "parentId": "myTransactionId2", + "skew": 0, }, - "timestamp": 1549324795824504, - }, - Object { - "childIds": Array [], - "docType": "span", - "duration": 481, - "id": "mySpanIdB", - "name": "SELECT FROM products", - "offset": 41627, "parentId": "mySpanIdA", - "serviceName": "opbeans-ruby", "skew": 0, - "span": Object { + }, + Object { + "doc": Object { "parent": Object { "id": "mySpanIdA", }, @@ -448,13 +555,13 @@ Object { }, "span": Object { "duration": Object { - "us": 481, + "us": 532, }, - "id": "mySpanIdB", - "name": "SELECT FROM products", + "id": "mySpanIdC", + "name": "SELECT FROM product", }, "timestamp": Object { - "us": 1549324795825633, + "us": 1549324795827905, }, "trace": Object { "id": "myTraceId", @@ -463,57 +570,223 @@ Object { "id": "myTransactionId2", }, }, - "timestamp": 1549324795825633, - }, - Object { - "childIds": Array [], "docType": "span", "duration": 532, "id": "mySpanIdC", - "name": "SELECT FROM product", "offset": 43899, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId2", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "span": Object { + "duration": Object { + "us": 6161, + }, + "id": "mySpanIdA", + "name": "Api::ProductsController#index", + }, + "timestamp": Object { + "us": 1549324795824504, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId2", + }, + }, + "docType": "span", + "duration": 6161, + "id": "mySpanIdA", + "offset": 40498, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "mySpanIdD", + }, + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "timestamp": Object { + "us": 1549324795823304, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 8634, + }, + "id": "myTransactionId2", + "marks": Object { + "agent": Object { + "domComplete": 383, + "domInteractive": 382, + "timeToFirstByte": 14, + }, + }, + "name": "Api::ProductsController#index", + }, + }, + "docType": "transaction", + "duration": 8634, + "id": "myTransactionId2", + "offset": 39298, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId1", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-node", + }, + "span": Object { + "duration": Object { + "us": 47557, + }, + "id": "mySpanIdD", + "name": "GET opbeans-ruby:3000/api/products", + }, + "timestamp": Object { + "us": 1549324795785760, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId1", + }, + }, + "docType": "span", + "duration": 47557, + "id": "mySpanIdD", + "offset": 1754, + "parent": Object { + "doc": Object { + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-node", + }, + "timestamp": Object { + "us": 1549324795784006, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 49660, + }, + "id": "myTransactionId1", + "name": "GET /api", + }, + }, + "docType": "transaction", + "duration": 49660, + "id": "myTransactionId1", + "offset": 0, + "parent": undefined, + "parentId": undefined, + "skew": 0, + }, + "parentId": "myTransactionId1", + "skew": 0, + }, + "parentId": "mySpanIdD", + "skew": 0, + }, + "parentId": "myTransactionId2", + "skew": 0, + }, "parentId": "mySpanIdA", - "serviceName": "opbeans-ruby", "skew": 0, - "span": Object { + }, + Object { + "doc": Object { + "agent": Object { + "name": "ruby", + "version": "2", + }, + "error": Object { + "grouping_key": "errorGroupingKey1", + "id": "error1", + "log": Object { + "message": "error message", + }, + }, "parent": Object { - "id": "mySpanIdA", + "id": "myTransactionId1", }, "processor": Object { - "event": "span", + "event": "error", }, "service": Object { "name": "opbeans-ruby", }, - "span": Object { - "duration": Object { - "us": 532, - }, - "id": "mySpanIdC", - "name": "SELECT FROM product", - }, "timestamp": Object { - "us": 1549324795827905, + "us": 1549324795810000, }, "trace": Object { "id": "myTraceId", }, "transaction": Object { - "id": "myTransactionId2", + "id": "myTransactionId1", + }, + }, + "docType": "error", + "duration": 0, + "id": "error1", + "offset": 25994, + "parent": Object { + "doc": Object { + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-node", + }, + "timestamp": Object { + "us": 1549324795784006, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 49660, + }, + "id": "myTransactionId1", + "name": "GET /api", + }, }, + "docType": "transaction", + "duration": 49660, + "id": "myTransactionId1", + "offset": 0, + "parent": undefined, + "parentId": undefined, + "skew": 0, }, - "timestamp": 1549324795827905, + "parentId": "myTransactionId1", + "skew": 0, }, ], - "serviceColors": Object { - "opbeans-node": "#3185fc", - "opbeans-ruby": "#00b3a4", - }, - "services": Array [ - "opbeans-node", - "opbeans-ruby", - ], - "traceRoot": Object { + "rootTransaction": Object { "processor": Object { "event": "transaction", }, @@ -534,7 +807,10 @@ Object { "name": "GET /api", }, }, - "traceRootDuration": 49660, + "serviceColors": Object { + "opbeans-node": "#3185fc", + "opbeans-ruby": "#00b3a4", + }, } `; @@ -562,221 +838,24 @@ Object { "us": 8634, }, "id": "myTransactionId2", - "name": "Api::ProductsController#index", - }, - }, - "errorCountByTransactionId": Object { - "myTransactionId1": 2, - "myTransactionId2": 3, - }, - "getTransactionById": [Function], - "itemsById": Object { - "mySpanIdA": Object { - "childIds": Array [ - "mySpanIdB", - "mySpanIdC", - ], - "docType": "span", - "duration": 6161, - "id": "mySpanIdA", - "name": "Api::ProductsController#index", - "offset": 1200, - "parentId": "myTransactionId2", - "serviceName": "opbeans-ruby", - "skew": 0, - "span": Object { - "parent": Object { - "id": "myTransactionId2", - }, - "processor": Object { - "event": "span", - }, - "service": Object { - "name": "opbeans-ruby", - }, - "span": Object { - "duration": Object { - "us": 6161, - }, - "id": "mySpanIdA", - "name": "Api::ProductsController#index", - }, - "timestamp": Object { - "us": 1549324795824504, - }, - "trace": Object { - "id": "myTraceId", - }, - "transaction": Object { - "id": "myTransactionId2", - }, - }, - "timestamp": 1549324795824504, - }, - "mySpanIdB": Object { - "childIds": Array [], - "docType": "span", - "duration": 481, - "id": "mySpanIdB", - "name": "SELECT FROM products", - "offset": 2329, - "parentId": "mySpanIdA", - "serviceName": "opbeans-ruby", - "skew": 0, - "span": Object { - "parent": Object { - "id": "mySpanIdA", - }, - "processor": Object { - "event": "span", - }, - "service": Object { - "name": "opbeans-ruby", - }, - "span": Object { - "duration": Object { - "us": 481, - }, - "id": "mySpanIdB", - "name": "SELECT FROM products", - }, - "timestamp": Object { - "us": 1549324795825633, - }, - "trace": Object { - "id": "myTraceId", - }, - "transaction": Object { - "id": "myTransactionId2", - }, - }, - "timestamp": 1549324795825633, - }, - "mySpanIdC": Object { - "childIds": Array [], - "docType": "span", - "duration": 532, - "id": "mySpanIdC", - "name": "SELECT FROM product", - "offset": 4601, - "parentId": "mySpanIdA", - "serviceName": "opbeans-ruby", - "skew": 0, - "span": Object { - "parent": Object { - "id": "mySpanIdA", - }, - "processor": Object { - "event": "span", - }, - "service": Object { - "name": "opbeans-ruby", - }, - "span": Object { - "duration": Object { - "us": 532, - }, - "id": "mySpanIdC", - "name": "SELECT FROM product", - }, - "timestamp": Object { - "us": 1549324795827905, - }, - "trace": Object { - "id": "myTraceId", - }, - "transaction": Object { - "id": "myTransactionId2", - }, - }, - "timestamp": 1549324795827905, - }, - "mySpanIdD": Object { - "docType": "span", - "duration": 47557, - "id": "mySpanIdD", - "name": "GET opbeans-ruby:3000/api/products", - "offset": 0, - "parentId": "myTransactionId1", - "serviceName": "opbeans-node", - "skew": 0, - "span": Object { - "parent": Object { - "id": "myTransactionId1", - }, - "processor": Object { - "event": "span", - }, - "service": Object { - "name": "opbeans-node", - }, - "span": Object { - "duration": Object { - "us": 47557, - }, - "id": "mySpanIdD", - "name": "GET opbeans-ruby:3000/api/products", - }, - "timestamp": Object { - "us": 1549324795785760, - }, - "trace": Object { - "id": "myTraceId", - }, - "transaction": Object { - "id": "myTransactionId1", - }, - }, - "timestamp": 1549324795785760, - }, - "myTransactionId1": Object { - "docType": "transaction", - "duration": 49660, - "errorCount": 2, - "id": "myTransactionId1", - "name": "GET /api", - "offset": 0, - "parentId": undefined, - "serviceName": "opbeans-node", - "skew": 0, - "timestamp": 1549324795784006, - "transaction": Object { - "processor": Object { - "event": "transaction", - }, - "service": Object { - "name": "opbeans-node", - }, - "timestamp": Object { - "us": 1549324795784006, - }, - "trace": Object { - "id": "myTraceId", - }, - "transaction": Object { - "duration": Object { - "us": 49660, - }, - "id": "myTransactionId1", - "name": "GET /api", + "marks": Object { + "agent": Object { + "domComplete": 383, + "domInteractive": 382, + "timeToFirstByte": 14, }, }, - }, - "myTransactionId2": Object { - "childIds": Array [ - "mySpanIdA", - ], - "docType": "transaction", - "duration": 8634, - "errorCount": 3, - "id": "myTransactionId2", "name": "Api::ProductsController#index", - "offset": 0, - "parentId": "mySpanIdD", - "serviceName": "opbeans-ruby", - "skew": 0, - "timestamp": 1549324795823304, - "transaction": Object { + }, + }, + "errorsCount": 0, + "errorsPerTransaction": Object { + "myTransactionId1": 2, + "myTransactionId2": 3, + }, + "items": Array [ + Object { + "doc": Object { "parent": Object { "id": "mySpanIdD", }, @@ -797,65 +876,26 @@ Object { "us": 8634, }, "id": "myTransactionId2", + "marks": Object { + "agent": Object { + "domComplete": 383, + "domInteractive": 382, + "timeToFirstByte": 14, + }, + }, "name": "Api::ProductsController#index", }, }, - }, - }, - "orderedItems": Array [ - Object { - "childIds": Array [ - "mySpanIdA", - ], "docType": "transaction", "duration": 8634, - "errorCount": 3, "id": "myTransactionId2", - "name": "Api::ProductsController#index", "offset": 0, + "parent": undefined, "parentId": "mySpanIdD", - "serviceName": "opbeans-ruby", "skew": 0, - "timestamp": 1549324795823304, - "transaction": Object { - "parent": Object { - "id": "mySpanIdD", - }, - "processor": Object { - "event": "transaction", - }, - "service": Object { - "name": "opbeans-ruby", - }, - "timestamp": Object { - "us": 1549324795823304, - }, - "trace": Object { - "id": "myTraceId", - }, - "transaction": Object { - "duration": Object { - "us": 8634, - }, - "id": "myTransactionId2", - "name": "Api::ProductsController#index", - }, - }, }, Object { - "childIds": Array [ - "mySpanIdB", - "mySpanIdC", - ], - "docType": "span", - "duration": 6161, - "id": "mySpanIdA", - "name": "Api::ProductsController#index", - "offset": 1200, - "parentId": "myTransactionId2", - "serviceName": "opbeans-ruby", - "skew": 0, - "span": Object { + "doc": Object { "parent": Object { "id": "myTransactionId2", }, @@ -882,19 +922,55 @@ Object { "id": "myTransactionId2", }, }, - "timestamp": 1549324795824504, - }, - Object { - "childIds": Array [], "docType": "span", - "duration": 481, - "id": "mySpanIdB", - "name": "SELECT FROM products", - "offset": 2329, - "parentId": "mySpanIdA", - "serviceName": "opbeans-ruby", + "duration": 6161, + "id": "mySpanIdA", + "offset": 1200, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "mySpanIdD", + }, + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "timestamp": Object { + "us": 1549324795823304, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 8634, + }, + "id": "myTransactionId2", + "marks": Object { + "agent": Object { + "domComplete": 383, + "domInteractive": 382, + "timeToFirstByte": 14, + }, + }, + "name": "Api::ProductsController#index", + }, + }, + "docType": "transaction", + "duration": 8634, + "id": "myTransactionId2", + "offset": 0, + "parent": undefined, + "parentId": "mySpanIdD", + "skew": 0, + }, + "parentId": "myTransactionId2", "skew": 0, - "span": Object { + }, + Object { + "doc": Object { "parent": Object { "id": "mySpanIdA", }, @@ -921,19 +997,90 @@ Object { "id": "myTransactionId2", }, }, - "timestamp": 1549324795825633, - }, - Object { - "childIds": Array [], "docType": "span", - "duration": 532, - "id": "mySpanIdC", - "name": "SELECT FROM product", - "offset": 4601, + "duration": 481, + "id": "mySpanIdB", + "offset": 2329, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId2", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "span": Object { + "duration": Object { + "us": 6161, + }, + "id": "mySpanIdA", + "name": "Api::ProductsController#index", + }, + "timestamp": Object { + "us": 1549324795824504, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId2", + }, + }, + "docType": "span", + "duration": 6161, + "id": "mySpanIdA", + "offset": 1200, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "mySpanIdD", + }, + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "timestamp": Object { + "us": 1549324795823304, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 8634, + }, + "id": "myTransactionId2", + "marks": Object { + "agent": Object { + "domComplete": 383, + "domInteractive": 382, + "timeToFirstByte": 14, + }, + }, + "name": "Api::ProductsController#index", + }, + }, + "docType": "transaction", + "duration": 8634, + "id": "myTransactionId2", + "offset": 0, + "parent": undefined, + "parentId": "mySpanIdD", + "skew": 0, + }, + "parentId": "myTransactionId2", + "skew": 0, + }, "parentId": "mySpanIdA", - "serviceName": "opbeans-ruby", "skew": 0, - "span": Object { + }, + Object { + "doc": Object { "parent": Object { "id": "mySpanIdA", }, @@ -960,16 +1107,90 @@ Object { "id": "myTransactionId2", }, }, - "timestamp": 1549324795827905, + "docType": "span", + "duration": 532, + "id": "mySpanIdC", + "offset": 4601, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "myTransactionId2", + }, + "processor": Object { + "event": "span", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "span": Object { + "duration": Object { + "us": 6161, + }, + "id": "mySpanIdA", + "name": "Api::ProductsController#index", + }, + "timestamp": Object { + "us": 1549324795824504, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId2", + }, + }, + "docType": "span", + "duration": 6161, + "id": "mySpanIdA", + "offset": 1200, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "mySpanIdD", + }, + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "timestamp": Object { + "us": 1549324795823304, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 8634, + }, + "id": "myTransactionId2", + "marks": Object { + "agent": Object { + "domComplete": 383, + "domInteractive": 382, + "timeToFirstByte": 14, + }, + }, + "name": "Api::ProductsController#index", + }, + }, + "docType": "transaction", + "duration": 8634, + "id": "myTransactionId2", + "offset": 0, + "parent": undefined, + "parentId": "mySpanIdD", + "skew": 0, + }, + "parentId": "myTransactionId2", + "skew": 0, + }, + "parentId": "mySpanIdA", + "skew": 0, }, ], - "serviceColors": Object { - "opbeans-ruby": "#3185fc", - }, - "services": Array [ - "opbeans-ruby", - ], - "traceRoot": Object { + "rootTransaction": Object { "processor": Object { "event": "transaction", }, @@ -990,30 +1211,61 @@ Object { "name": "GET /api", }, }, - "traceRootDuration": 49660, + "serviceColors": Object { + "opbeans-ruby": "#3185fc", + }, } `; exports[`waterfall_helpers getWaterfallItems should handle cyclic references 1`] = ` Array [ Object { - "childIds": Array [ - "a", - ], + "doc": Object { + "timestamp": Object { + "us": 10, + }, + "transaction": Object { + "id": "a", + }, + }, + "docType": "transaction", "id": "a", "offset": 0, + "parent": undefined, "skew": 0, - "timestamp": 10, }, Object { - "childIds": Array [ - "a", - ], - "id": "a", + "doc": Object { + "parent": Object { + "id": "a", + }, + "span": Object { + "id": "b", + }, + "timestamp": Object { + "us": 20, + }, + }, + "docType": "span", + "id": "b", "offset": 10, + "parent": Object { + "doc": Object { + "timestamp": Object { + "us": 10, + }, + "transaction": Object { + "id": "a", + }, + }, + "docType": "transaction", + "id": "a", + "offset": 0, + "parent": undefined, + "skew": 0, + }, "parentId": "a", - "skew": undefined, - "timestamp": 20, + "skew": 0, }, ] `; @@ -1021,89 +1273,280 @@ Array [ exports[`waterfall_helpers getWaterfallItems should order items correctly 1`] = ` Array [ Object { - "childIds": Array [ - "b2", - "b", - ], + "doc": Object { + "service": Object { + "name": "opbeans-java", + }, + "timestamp": Object { + "us": 1536763736366000, + }, + "transaction": Object { + "id": "a", + "name": "APIRestController#products", + }, + }, "docType": "transaction", "duration": 9480, - "errorCount": 0, "id": "a", - "name": "APIRestController#products", "offset": 0, - "serviceName": "opbeans-java", + "parent": undefined, "skew": 0, - "timestamp": 1536763736366000, - "transaction": Object {}, }, Object { - "childIds": Array [], + "doc": Object { + "parent": Object { + "id": "a", + }, + "service": Object { + "name": "opbeans-java", + }, + "span": Object { + "id": "b2", + "name": "GET [0:0:0:0:0:0:0:1]", + }, + "timestamp": Object { + "us": 1536763736367000, + }, + "transaction": Object { + "id": "a", + }, + }, "docType": "span", "duration": 4694, "id": "b2", - "name": "GET [0:0:0:0:0:0:0:1]", "offset": 1000, + "parent": Object { + "doc": Object { + "service": Object { + "name": "opbeans-java", + }, + "timestamp": Object { + "us": 1536763736366000, + }, + "transaction": Object { + "id": "a", + "name": "APIRestController#products", + }, + }, + "docType": "transaction", + "duration": 9480, + "id": "a", + "offset": 0, + "parent": undefined, + "skew": 0, + }, "parentId": "a", - "serviceName": "opbeans-java", "skew": 0, - "span": Object { + }, + Object { + "doc": Object { + "parent": Object { + "id": "a", + }, + "service": Object { + "name": "opbeans-java", + }, + "span": Object { + "id": "b", + "name": "GET [0:0:0:0:0:0:0:1]", + }, + "timestamp": Object { + "us": 1536763736368000, + }, "transaction": Object { "id": "a", }, }, - "timestamp": 1536763736367000, - }, - Object { - "childIds": Array [ - "c", - ], "docType": "span", "duration": 4694, "id": "b", - "name": "GET [0:0:0:0:0:0:0:1]", "offset": 2000, + "parent": Object { + "doc": Object { + "service": Object { + "name": "opbeans-java", + }, + "timestamp": Object { + "us": 1536763736366000, + }, + "transaction": Object { + "id": "a", + "name": "APIRestController#products", + }, + }, + "docType": "transaction", + "duration": 9480, + "id": "a", + "offset": 0, + "parent": undefined, + "skew": 0, + }, "parentId": "a", - "serviceName": "opbeans-java", "skew": 0, - "span": Object { + }, + Object { + "doc": Object { + "parent": Object { + "id": "b", + }, + "service": Object { + "name": "opbeans-java", + }, + "timestamp": Object { + "us": 1536763736369000, + }, "transaction": Object { - "id": "a", + "id": "c", + "name": "APIRestController#productsRemote", }, }, - "timestamp": 1536763736368000, - }, - Object { - "childIds": Array [ - "d", - ], "docType": "transaction", "duration": 3581, - "errorCount": 0, "id": "c", - "name": "APIRestController#productsRemote", "offset": 3000, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "a", + }, + "service": Object { + "name": "opbeans-java", + }, + "span": Object { + "id": "b", + "name": "GET [0:0:0:0:0:0:0:1]", + }, + "timestamp": Object { + "us": 1536763736368000, + }, + "transaction": Object { + "id": "a", + }, + }, + "docType": "span", + "duration": 4694, + "id": "b", + "offset": 2000, + "parent": Object { + "doc": Object { + "service": Object { + "name": "opbeans-java", + }, + "timestamp": Object { + "us": 1536763736366000, + }, + "transaction": Object { + "id": "a", + "name": "APIRestController#products", + }, + }, + "docType": "transaction", + "duration": 9480, + "id": "a", + "offset": 0, + "parent": undefined, + "skew": 0, + }, + "parentId": "a", + "skew": 0, + }, "parentId": "b", - "serviceName": "opbeans-java", "skew": 0, - "timestamp": 1536763736369000, - "transaction": Object {}, }, Object { - "childIds": Array [], + "doc": Object { + "parent": Object { + "id": "c", + }, + "service": Object { + "name": "opbeans-java", + }, + "span": Object { + "id": "d", + "name": "SELECT", + }, + "timestamp": Object { + "us": 1536763736371000, + }, + "transaction": Object { + "id": "c", + }, + }, "docType": "span", "duration": 210, "id": "d", - "name": "SELECT", "offset": 5000, - "parentId": "c", - "serviceName": "opbeans-java", - "skew": 0, - "span": Object { - "transaction": Object { - "id": "c", + "parent": Object { + "doc": Object { + "parent": Object { + "id": "b", + }, + "service": Object { + "name": "opbeans-java", + }, + "timestamp": Object { + "us": 1536763736369000, + }, + "transaction": Object { + "id": "c", + "name": "APIRestController#productsRemote", + }, + }, + "docType": "transaction", + "duration": 3581, + "id": "c", + "offset": 3000, + "parent": Object { + "doc": Object { + "parent": Object { + "id": "a", + }, + "service": Object { + "name": "opbeans-java", + }, + "span": Object { + "id": "b", + "name": "GET [0:0:0:0:0:0:0:1]", + }, + "timestamp": Object { + "us": 1536763736368000, + }, + "transaction": Object { + "id": "a", + }, + }, + "docType": "span", + "duration": 4694, + "id": "b", + "offset": 2000, + "parent": Object { + "doc": Object { + "service": Object { + "name": "opbeans-java", + }, + "timestamp": Object { + "us": 1536763736366000, + }, + "transaction": Object { + "id": "a", + "name": "APIRestController#products", + }, + }, + "docType": "transaction", + "duration": 9480, + "id": "a", + "offset": 0, + "parent": undefined, + "skew": 0, + }, + "parentId": "a", + "skew": 0, }, + "parentId": "b", + "skew": 0, }, - "timestamp": 1536763736371000, + "parentId": "c", + "skew": 0, }, ] `; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts index 6166515fd9d38..426842bc02f51 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts @@ -11,8 +11,10 @@ import { getClockSkew, getOrderedWaterfallItems, getWaterfall, - IWaterfallItem + IWaterfallItem, + IWaterfallTransaction } from './waterfall_helpers'; +import { APMError } from '../../../../../../../../typings/es_schemas/ui/APMError'; describe('waterfall_helpers', () => { describe('getWaterfall', () => { @@ -80,7 +82,7 @@ describe('waterfall_helpers', () => { }, timestamp: { us: 1549324795785760 } } as Span, - { + ({ parent: { id: 'mySpanIdD' }, processor: { event: 'transaction' }, trace: { id: 'myTraceId' }, @@ -88,10 +90,36 @@ describe('waterfall_helpers', () => { transaction: { duration: { us: 8634 }, name: 'Api::ProductsController#index', - id: 'myTransactionId2' + id: 'myTransactionId2', + marks: { + agent: { + domInteractive: 382, + domComplete: 383, + timeToFirstByte: 14 + } + } }, timestamp: { us: 1549324795823304 } - } as Transaction + } as unknown) as Transaction, + ({ + processor: { event: 'error' }, + parent: { id: 'myTransactionId1' }, + timestamp: { us: 1549324795810000 }, + trace: { id: 'myTraceId' }, + transaction: { id: 'myTransactionId1' }, + error: { + id: 'error1', + grouping_key: 'errorGroupingKey1', + log: { + message: 'error message' + } + }, + service: { name: 'opbeans-ruby' }, + agent: { + name: 'ruby', + version: '2' + } + } as unknown) as APMError ]; it('should return full waterfall', () => { @@ -107,8 +135,10 @@ describe('waterfall_helpers', () => { }, entryTransactionId ); - expect(waterfall.orderedItems.length).toBe(6); - expect(waterfall.orderedItems[0].id).toBe('myTransactionId1'); + + expect(waterfall.items.length).toBe(7); + expect(waterfall.items[0].id).toBe('myTransactionId1'); + expect(waterfall.errorsCount).toEqual(1); expect(waterfall).toMatchSnapshot(); }); @@ -125,26 +155,11 @@ describe('waterfall_helpers', () => { }, entryTransactionId ); - expect(waterfall.orderedItems.length).toBe(4); - expect(waterfall.orderedItems[0].id).toBe('myTransactionId2'); - expect(waterfall).toMatchSnapshot(); - }); - it('getTransactionById', () => { - const entryTransactionId = 'myTransactionId1'; - const errorsPerTransaction = { - myTransactionId1: 2, - myTransactionId2: 3 - }; - const waterfall = getWaterfall( - { - trace: { items: hits, exceedsMax: false }, - errorsPerTransaction - }, - entryTransactionId - ); - const transaction = waterfall.getTransactionById('myTransactionId2'); - expect(transaction!.transaction.id).toBe('myTransactionId2'); + expect(waterfall.items.length).toBe(4); + expect(waterfall.items[0].id).toBe('myTransactionId2'); + expect(waterfall.errorsCount).toEqual(0); + expect(waterfall).toMatchSnapshot(); }); }); @@ -152,84 +167,102 @@ describe('waterfall_helpers', () => { it('should order items correctly', () => { const items: IWaterfallItem[] = [ { + docType: 'span', + doc: { + parent: { id: 'c' }, + service: { name: 'opbeans-java' }, + transaction: { + id: 'c' + }, + timestamp: { us: 1536763736371000 }, + span: { + id: 'd', + name: 'SELECT' + } + } as Span, id: 'd', parentId: 'c', - serviceName: 'opbeans-java', - name: 'SELECT', duration: 210, - timestamp: 1536763736371000, offset: 0, - skew: 0, + skew: 0 + }, + { docType: 'span', - span: { + doc: { + parent: { id: 'a' }, + service: { name: 'opbeans-java' }, transaction: { - id: 'c' + id: 'a' + }, + timestamp: { us: 1536763736368000 }, + span: { + id: 'b', + name: 'GET [0:0:0:0:0:0:0:1]' } - } as Span - }, - { + } as Span, id: 'b', parentId: 'a', - serviceName: 'opbeans-java', - name: 'GET [0:0:0:0:0:0:0:1]', duration: 4694, - timestamp: 1536763736368000, offset: 0, - skew: 0, + skew: 0 + }, + { docType: 'span', - span: { + doc: { + parent: { id: 'a' }, + service: { name: 'opbeans-java' }, transaction: { id: 'a' + }, + timestamp: { us: 1536763736367000 }, + span: { + id: 'b2', + name: 'GET [0:0:0:0:0:0:0:1]' } - } as Span - }, - { + } as Span, id: 'b2', parentId: 'a', - serviceName: 'opbeans-java', - name: 'GET [0:0:0:0:0:0:0:1]', duration: 4694, - timestamp: 1536763736367000, offset: 0, - skew: 0, - docType: 'span', - span: { - transaction: { - id: 'a' - } - } as Span + skew: 0 }, { + docType: 'transaction', + doc: { + parent: { id: 'b' }, + service: { name: 'opbeans-java' }, + timestamp: { us: 1536763736369000 }, + transaction: { id: 'c', name: 'APIRestController#productsRemote' } + } as Transaction, id: 'c', parentId: 'b', - serviceName: 'opbeans-java', - name: 'APIRestController#productsRemote', duration: 3581, - timestamp: 1536763736369000, offset: 0, - skew: 0, - docType: 'transaction', - transaction: {} as Transaction, - errorCount: 0 + skew: 0 }, { + docType: 'transaction', + doc: { + service: { name: 'opbeans-java' }, + timestamp: { us: 1536763736366000 }, + transaction: { + id: 'a', + name: 'APIRestController#products' + } + } as Transaction, id: 'a', - serviceName: 'opbeans-java', - name: 'APIRestController#products', duration: 9480, - timestamp: 1536763736366000, offset: 0, - skew: 0, - docType: 'transaction', - transaction: {} as Transaction, - errorCount: 0 + skew: 0 } ]; const childrenByParentId = groupBy(items, hit => hit.parentId ? hit.parentId : 'root' ); - const entryTransactionItem = childrenByParentId.root[0]; + const entryTransactionItem = childrenByParentId + .root[0] as IWaterfallTransaction; + expect( getOrderedWaterfallItems(childrenByParentId, entryTransactionItem) ).toMatchSnapshot(); @@ -237,13 +270,32 @@ describe('waterfall_helpers', () => { it('should handle cyclic references', () => { const items = [ - { id: 'a', timestamp: 10 } as IWaterfallItem, - { id: 'a', parentId: 'a', timestamp: 20 } as IWaterfallItem + { + docType: 'transaction', + id: 'a', + doc: ({ + transaction: { id: 'a' }, + timestamp: { us: 10 } + } as unknown) as Transaction + } as IWaterfallItem, + { + docType: 'span', + id: 'b', + parentId: 'a', + doc: ({ + span: { + id: 'b' + }, + parent: { id: 'a' }, + timestamp: { us: 20 } + } as unknown) as Span + } as IWaterfallItem ]; const childrenByParentId = groupBy(items, hit => hit.parentId ? hit.parentId : 'root' ); - const entryTransactionItem = childrenByParentId.root[0]; + const entryTransactionItem = childrenByParentId + .root[0] as IWaterfallTransaction; expect( getOrderedWaterfallItems(childrenByParentId, entryTransactionItem) ).toMatchSnapshot(); @@ -254,12 +306,17 @@ describe('waterfall_helpers', () => { it('should adjust when child starts before parent', () => { const child = { docType: 'transaction', - timestamp: 0, + doc: { + timestamp: { us: 0 } + }, duration: 50 } as IWaterfallItem; const parent = { - timestamp: 100, + docType: 'transaction', + doc: { + timestamp: { us: 100 } + }, duration: 100, skew: 5 } as IWaterfallItem; @@ -270,12 +327,17 @@ describe('waterfall_helpers', () => { it('should not adjust when child starts after parent has ended', () => { const child = { docType: 'transaction', - timestamp: 250, + doc: { + timestamp: { us: 250 } + }, duration: 50 } as IWaterfallItem; const parent = { - timestamp: 100, + docType: 'transaction', + doc: { + timestamp: { us: 100 } + }, duration: 100, skew: 5 } as IWaterfallItem; @@ -286,12 +348,17 @@ describe('waterfall_helpers', () => { it('should not adjust when child starts within parent duration', () => { const child = { docType: 'transaction', - timestamp: 150, + doc: { + timestamp: { us: 150 } + }, duration: 50 } as IWaterfallItem; const parent = { - timestamp: 100, + docType: 'transaction', + doc: { + timestamp: { us: 100 } + }, duration: 100, skew: 5 } as IWaterfallItem; @@ -305,7 +372,27 @@ describe('waterfall_helpers', () => { } as IWaterfallItem; const parent = { - timestamp: 100, + docType: 'span', + doc: { + timestamp: { us: 100 } + }, + duration: 100, + skew: 5 + } as IWaterfallItem; + + expect(getClockSkew(child, parent)).toBe(5); + }); + + it('should return parent skew for errors', () => { + const child = { + docType: 'error' + } as IWaterfallItem; + + const parent = { + docType: 'transaction', + doc: { + timestamp: { us: 100 } + }, duration: 100, skew: 5 } as IWaterfallItem; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts index 2a69c5f51173d..1af6cddb3ba4a 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts @@ -6,60 +6,52 @@ import theme from '@elastic/eui/dist/eui_theme_light.json'; import { + first, flatten, groupBy, - indexBy, + isEmpty, sortBy, + sum, uniq, - zipObject, - isEmpty, - first + zipObject } from 'lodash'; import { TraceAPIResponse } from '../../../../../../../../server/lib/traces/get_trace'; +import { APMError } from '../../../../../../../../typings/es_schemas/ui/APMError'; import { Span } from '../../../../../../../../typings/es_schemas/ui/Span'; import { Transaction } from '../../../../../../../../typings/es_schemas/ui/Transaction'; -interface IWaterfallIndex { - [key: string]: IWaterfallItem | undefined; -} - interface IWaterfallGroup { [key: string]: IWaterfallItem[]; } export interface IWaterfall { entryTransaction?: Transaction; - traceRoot?: Transaction; - traceRootDuration?: number; + rootTransaction?: Transaction; /** * Duration in us */ duration: number; - services: string[]; - orderedItems: IWaterfallItem[]; - itemsById: IWaterfallIndex; - getTransactionById: (id?: IWaterfallItem['id']) => Transaction | undefined; - errorCountByTransactionId: TraceAPIResponse['errorsPerTransaction']; + items: IWaterfallItem[]; + errorsPerTransaction: TraceAPIResponse['errorsPerTransaction']; + errorsCount: number; serviceColors: IServiceColors; } -interface IWaterfallItemBase { - id: string | number; +interface IWaterfallItemBase { + docType: U; + doc: T; + + id: string; + + parent?: IWaterfallItem; parentId?: string; - serviceName: string; - name: string; /** * Duration in us */ duration: number; - /** - * start timestamp in us - */ - timestamp: number; - /** * offset from first item in us */ @@ -69,53 +61,53 @@ interface IWaterfallItemBase { * skew from timestamp in us */ skew: number; - childIds?: Array; -} - -interface IWaterfallItemTransaction extends IWaterfallItemBase { - transaction: Transaction; - docType: 'transaction'; - errorCount: number; } -interface IWaterfallItemSpan extends IWaterfallItemBase { - span: Span; - docType: 'span'; -} +export type IWaterfallTransaction = IWaterfallItemBase< + Transaction, + 'transaction' +>; +export type IWaterfallSpan = IWaterfallItemBase; +export type IWaterfallError = IWaterfallItemBase; -export type IWaterfallItem = IWaterfallItemSpan | IWaterfallItemTransaction; +export type IWaterfallItem = + | IWaterfallTransaction + | IWaterfallSpan + | IWaterfallError; -function getTransactionItem( - transaction: Transaction, - errorsPerTransaction: TraceAPIResponse['errorsPerTransaction'] -): IWaterfallItemTransaction { +function getTransactionItem(transaction: Transaction): IWaterfallTransaction { return { + docType: 'transaction', + doc: transaction, id: transaction.transaction.id, - parentId: transaction.parent && transaction.parent.id, - serviceName: transaction.service.name, - name: transaction.transaction.name, + parentId: transaction.parent?.id, duration: transaction.transaction.duration.us, - timestamp: transaction.timestamp.us, offset: 0, - skew: 0, - docType: 'transaction', - transaction, - errorCount: errorsPerTransaction[transaction.transaction.id] || 0 + skew: 0 }; } -function getSpanItem(span: Span): IWaterfallItemSpan { +function getSpanItem(span: Span): IWaterfallSpan { return { + docType: 'span', + doc: span, id: span.span.id, - parentId: span.parent && span.parent.id, - serviceName: span.service.name, - name: span.span.name, + parentId: span.parent?.id, duration: span.span.duration.us, - timestamp: span.timestamp.us, + offset: 0, + skew: 0 + }; +} + +function getErrorItem(error: APMError): IWaterfallError { + return { + docType: 'error', + doc: error, + id: error.error.id, + parentId: error.parent?.id, offset: 0, skew: 0, - docType: 'span', - span + duration: 0 }; } @@ -126,18 +118,17 @@ export function getClockSkew( if (!parentItem) { return 0; } - switch (item.docType) { - // don't calculate skew for spans. Just use parent's skew + // don't calculate skew for spans and errors. Just use parent's skew + case 'error': case 'span': return parentItem.skew; - // transaction is the inital entry in a service. Calculate skew for this, and it will be propogated to all child spans case 'transaction': { - const parentStart = parentItem.timestamp + parentItem.skew; + const parentStart = parentItem.doc.timestamp.us + parentItem.skew; // determine if child starts before the parent - const offsetStart = parentStart - item.timestamp; + const offsetStart = parentStart - item.doc.timestamp.us; if (offsetStart > 0) { const latency = Math.max(parentItem.duration - item.duration, 0) / 2; return offsetStart + latency; @@ -151,9 +142,14 @@ export function getClockSkew( export function getOrderedWaterfallItems( childrenByParentId: IWaterfallGroup, - entryTransactionItem: IWaterfallItem + entryWaterfallTransaction?: IWaterfallTransaction ) { + if (!entryWaterfallTransaction) { + return []; + } + const entryTimestamp = entryWaterfallTransaction.doc.timestamp.us; const visitedWaterfallItemSet = new Set(); + function getSortedChildren( item: IWaterfallItem, parentItem?: IWaterfallItem @@ -162,10 +158,16 @@ export function getOrderedWaterfallItems( return []; } visitedWaterfallItemSet.add(item); - const children = sortBy(childrenByParentId[item.id] || [], 'timestamp'); - item.childIds = children.map(child => child.id); - item.offset = item.timestamp - entryTransactionItem.timestamp; + const children = sortBy( + childrenByParentId[item.id] || [], + 'doc.timestamp.us' + ); + + item.parent = parentItem; + // get offset from the beginning of trace + item.offset = item.doc.timestamp.us - entryTimestamp; + // move the item to the right if it starts before its parent item.skew = getClockSkew(item, parentItem); const deepChildren = flatten( @@ -174,24 +176,21 @@ export function getOrderedWaterfallItems( return [item, ...deepChildren]; } - return getSortedChildren(entryTransactionItem); + return getSortedChildren(entryWaterfallTransaction); } -function getTraceRoot(childrenByParentId: IWaterfallGroup) { +function getRootTransaction(childrenByParentId: IWaterfallGroup) { const item = first(childrenByParentId.root); if (item && item.docType === 'transaction') { - return item.transaction; + return item.doc; } } -function getServices(items: IWaterfallItem[]) { - const serviceNames = items.map(item => item.serviceName); - return uniq(serviceNames); -} - export type IServiceColors = Record; -function getServiceColors(services: string[]) { +function getServiceColors(waterfallItems: IWaterfallItem[]) { + const services = uniq(waterfallItems.map(item => item.doc.service.name)); + const assignedColors = [ theme.euiColorVis1, theme.euiColorVis0, @@ -205,30 +204,35 @@ function getServiceColors(services: string[]) { return zipObject(services, assignedColors) as IServiceColors; } -function getDuration(items: IWaterfallItem[]) { - if (items.length === 0) { - return 0; - } - const timestampStart = items[0].timestamp; - const timestampEnd = Math.max( - ...items.map(item => item.timestamp + item.duration + item.skew) +const getWaterfallDuration = (waterfallItems: IWaterfallItem[]) => + Math.max( + ...waterfallItems.map(item => item.offset + item.skew + item.duration), + 0 ); - return timestampEnd - timestampStart; -} -function createGetTransactionById(itemsById: IWaterfallIndex) { - return (id?: IWaterfallItem['id']) => { - if (!id) { - return undefined; +const getWaterfallItems = (items: TraceAPIResponse['trace']['items']) => + items.map(item => { + const docType = item.processor.event; + switch (docType) { + case 'span': + return getSpanItem(item as Span); + case 'transaction': + return getTransactionItem(item as Transaction); + case 'error': + return getErrorItem(item as APMError); } + }); - const item = itemsById[id]; - const isTransaction = item?.docType === 'transaction'; - if (isTransaction) { - return (item as IWaterfallItemTransaction).transaction; - } - }; -} +const getChildrenGroupedByParentId = (waterfallItems: IWaterfallItem[]) => + groupBy(waterfallItems, item => (item.parentId ? item.parentId : 'root')); + +const getEntryWaterfallTransaction = ( + entryTransactionId: string, + waterfallItems: IWaterfallItem[] +): IWaterfallTransaction | undefined => + waterfallItems.find( + item => item.docType === 'transaction' && item.id === entryTransactionId + ) as IWaterfallTransaction; export function getWaterfall( { trace, errorsPerTransaction }: TraceAPIResponse, @@ -236,59 +240,41 @@ export function getWaterfall( ): IWaterfall { if (isEmpty(trace.items) || !entryTransactionId) { return { - services: [], duration: 0, - orderedItems: [], - itemsById: {}, - getTransactionById: () => undefined, - errorCountByTransactionId: errorsPerTransaction, + items: [], + errorsPerTransaction, + errorsCount: sum(Object.values(errorsPerTransaction)), serviceColors: {} }; } - const waterfallItems = trace.items.map(traceItem => { - const docType = traceItem.processor.event; - switch (docType) { - case 'span': - return getSpanItem(traceItem as Span); - case 'transaction': - return getTransactionItem( - traceItem as Transaction, - errorsPerTransaction - ); - } - }); + const waterfallItems: IWaterfallItem[] = getWaterfallItems(trace.items); + + const childrenByParentId = getChildrenGroupedByParentId(waterfallItems); - const childrenByParentId = groupBy(waterfallItems, item => - item.parentId ? item.parentId : 'root' + const entryWaterfallTransaction = getEntryWaterfallTransaction( + entryTransactionId, + waterfallItems ); - const entryTransactionItem = waterfallItems.find( - waterfallItem => - waterfallItem.docType === 'transaction' && - waterfallItem.id === entryTransactionId + + const items = getOrderedWaterfallItems( + childrenByParentId, + entryWaterfallTransaction ); - const itemsById: IWaterfallIndex = indexBy(waterfallItems, 'id'); - const orderedItems = entryTransactionItem - ? getOrderedWaterfallItems(childrenByParentId, entryTransactionItem) - : []; - const traceRoot = getTraceRoot(childrenByParentId); - const duration = getDuration(orderedItems); - const traceRootDuration = traceRoot && traceRoot.transaction.duration.us; - const services = getServices(orderedItems); - const getTransactionById = createGetTransactionById(itemsById); - const serviceColors = getServiceColors(services); - const entryTransaction = getTransactionById(entryTransactionId); + + const rootTransaction = getRootTransaction(childrenByParentId); + const duration = getWaterfallDuration(items); + const serviceColors = getServiceColors(items); + + const entryTransaction = entryWaterfallTransaction?.doc; return { entryTransaction, - traceRoot, - traceRootDuration, + rootTransaction, duration, - services, - orderedItems, - itemsById, - getTransactionById, - errorCountByTransactionId: errorsPerTransaction, + items, + errorsPerTransaction, + errorsCount: items.filter(item => item.docType === 'error').length, serviceColors }; } diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/get_agent_marks.ts b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/get_agent_marks.ts deleted file mode 100644 index af76451db68b7..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/get_agent_marks.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { sortBy } from 'lodash'; -import { Transaction } from '../../../../../../typings/es_schemas/ui/Transaction'; - -export interface AgentMark { - name: string; - us: number; -} - -export function getAgentMarks(transaction: Transaction): AgentMark[] { - if (!(transaction.transaction.marks && transaction.transaction.marks.agent)) { - return []; - } - - return sortBy( - Object.entries(transaction.transaction.marks.agent).map(([name, ms]) => ({ - name, - us: ms * 1000 - })), - 'us' - ); -} diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/index.tsx index 2f34cc86c5cfc..77be5c999f7c3 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/index.tsx @@ -6,16 +6,13 @@ import { Location } from 'history'; import React from 'react'; -import { Transaction } from '../../../../../../typings/es_schemas/ui/Transaction'; import { IUrlParams } from '../../../../../context/UrlParamsContext/types'; -import { getAgentMarks } from './get_agent_marks'; import { ServiceLegends } from './ServiceLegends'; import { Waterfall } from './Waterfall'; import { IWaterfall } from './Waterfall/waterfall_helpers/waterfall_helpers'; interface Props { urlParams: IUrlParams; - transaction: Transaction; location: Location; waterfall: IWaterfall; exceedsMax: boolean; @@ -24,11 +21,9 @@ interface Props { export function WaterfallContainer({ location, urlParams, - transaction, waterfall, exceedsMax }: Props) { - const agentMarks = getAgentMarks(transaction); if (!waterfall) { return null; } @@ -37,10 +32,8 @@ export function WaterfallContainer({
diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/__tests__/ErrorCount.test.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/__tests__/ErrorCount.test.tsx new file mode 100644 index 0000000000000..62b5f7834d3a9 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/__tests__/ErrorCount.test.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { fireEvent, render } from '@testing-library/react'; +import React from 'react'; +import { expectTextsInDocument } from '../../../../../utils/testHelpers'; +import { ErrorCount } from '../ErrorCount'; + +describe('ErrorCount', () => { + it('shows singular error message', () => { + const component = render(); + expectTextsInDocument(component, ['1 Error']); + }); + it('shows plural error message', () => { + const component = render(); + expectTextsInDocument(component, ['2 Errors']); + }); + it('prevents click propagation', () => { + const mock = jest.fn(); + const { getByText } = render( + + ); + fireEvent( + getByText('1 Error'), + new MouseEvent('click', { + bubbles: true, + cancelable: true + }) + ); + expect(mock).not.toHaveBeenCalled(); + }); +}); diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx index b56370a59c8e2..6dcab6c6b97c1 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx @@ -5,30 +5,29 @@ */ import { + EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, + EuiPagination, EuiPanel, EuiSpacer, - EuiEmptyPrompt, - EuiTitle, - EuiPagination + EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { Location } from 'history'; -import React, { useState, useEffect } from 'react'; -import { sum } from 'lodash'; +import React, { useEffect, useState } from 'react'; import styled from 'styled-components'; -import { IUrlParams } from '../../../../context/UrlParamsContext/types'; -import { TransactionActionMenu } from '../../../shared/TransactionActionMenu/TransactionActionMenu'; -import { TransactionTabs } from './TransactionTabs'; -import { IWaterfall } from './WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers'; -import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; -import { TransactionSummary } from '../../../shared/Summary/TransactionSummary'; import { IBucket } from '../../../../../server/lib/transactions/distribution/get_buckets/transform'; +import { IUrlParams } from '../../../../context/UrlParamsContext/types'; +import { px, units } from '../../../../style/variables'; import { history } from '../../../../utils/history'; import { fromQuery, toQuery } from '../../../shared/Links/url_helpers'; +import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; +import { TransactionSummary } from '../../../shared/Summary/TransactionSummary'; +import { TransactionActionMenu } from '../../../shared/TransactionActionMenu/TransactionActionMenu'; import { MaybeViewTraceLink } from './MaybeViewTraceLink'; -import { units, px } from '../../../../style/variables'; +import { TransactionTabs } from './TransactionTabs'; +import { IWaterfall } from './WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers'; const PaginationContainer = styled.div` margin-left: ${px(units.quarter)}; @@ -140,8 +139,8 @@ export const WaterfallWithSummmary: React.FC = ({ diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/APMLink.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/APMLink.tsx index 0312e94d7ee19..eba59f6e3ce44 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/APMLink.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/APMLink.tsx @@ -18,7 +18,7 @@ interface Props extends EuiLinkAnchorProps { children?: React.ReactNode; } -export type APMLinkExtendProps = Omit; +export type APMLinkExtendProps = Omit; export const PERSISTENT_APM_PARAMS = [ 'kuery', diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItem.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx similarity index 51% rename from x-pack/legacy/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItem.tsx rename to x-pack/legacy/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx index 964debbedb2e4..7558f002c0afc 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItem.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx @@ -6,28 +6,25 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; +import { EuiBadge } from '@elastic/eui'; +import euiThemeLight from '@elastic/eui/dist/eui_theme_light.json'; import { px } from '../../../../public/style/variables'; -import { ErrorCountBadge } from '../../app/TransactionDetails/WaterfallWithSummmary/ErrorCountBadge'; import { units } from '../../../style/variables'; interface Props { count: number; } -const Badge = styled(ErrorCountBadge)` +const Badge = styled(EuiBadge)` margin-top: ${px(units.eighth)}; `; -const ErrorCountSummaryItem = ({ count }: Props) => { - return ( - - {i18n.translate('xpack.apm.transactionDetails.errorCount', { - defaultMessage: - '{errorCount, number} {errorCount, plural, one {Error} other {Errors}}', - values: { errorCount: count } - })} - - ); -}; - -export { ErrorCountSummaryItem }; +export const ErrorCountSummaryItemBadge = ({ count }: Props) => ( + + {i18n.translate('xpack.apm.transactionDetails.errorCount', { + defaultMessage: + '{errorCount, number} {errorCount, plural, one {Error} other {Errors}}', + values: { errorCount: count } + })} + +); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx index 8b7380a18edc3..51da61cd7c1a6 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx @@ -8,7 +8,7 @@ import { Transaction } from '../../../../typings/es_schemas/ui/Transaction'; import { Summary } from './'; import { TimestampTooltip } from '../TimestampTooltip'; import { DurationSummaryItem } from './DurationSummaryItem'; -import { ErrorCountSummaryItem } from './ErrorCountSummaryItem'; +import { ErrorCountSummaryItemBadge } from './ErrorCountSummaryItemBadge'; import { isRumAgentName } from '../../../../common/agent_name'; import { HttpInfoSummaryItem } from './HttpInfoSummaryItem'; import { TransactionResultSummaryItem } from './TransactionResultSummaryItem'; @@ -54,7 +54,7 @@ const TransactionSummary = ({ parentType="trace" />, getTransactionResultSummaryItem(transaction), - errorCount ? : null, + errorCount ? : null, transaction.user_agent ? ( ) : null diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/__test__/ErrorCountSummaryItemBadge.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Summary/__test__/ErrorCountSummaryItemBadge.test.tsx new file mode 100644 index 0000000000000..33f5752b6389b --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/shared/Summary/__test__/ErrorCountSummaryItemBadge.test.tsx @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { ErrorCountSummaryItemBadge } from '../ErrorCountSummaryItemBadge'; +import { render } from '@testing-library/react'; +import { expectTextsInDocument } from '../../../../utils/testHelpers'; + +describe('ErrorCountSummaryItemBadge', () => { + it('shows singular error message', () => { + const component = render(); + expectTextsInDocument(component, ['1 Error']); + }); + it('shows plural error message', () => { + const component = render(); + expectTextsInDocument(component, ['2 Errors']); + }); +}); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/Legends.js b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/Legends.js index 848c975942ff6..99eb17386f847 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/Legends.js +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/Legends.js @@ -7,7 +7,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import styled from 'styled-components'; -import Legend from '../Legend'; +import { Legend } from '../Legend'; import { unit, units, diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap index c46cbbbcccc0b..557751a0f0226 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap @@ -2725,11 +2725,14 @@ Array [ @@ -2763,11 +2766,14 @@ Array [ @@ -2794,11 +2800,14 @@ Array [ @@ -5167,11 +5176,14 @@ Array [ Avg. @@ -5210,11 +5222,14 @@ Array [ 95th @@ -5253,11 +5268,14 @@ Array [ 99th @@ -5886,11 +5904,14 @@ Array [ @@ -5924,11 +5945,14 @@ Array [ @@ -5955,11 +5979,14 @@ Array [ diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Legend/index.js b/x-pack/legacy/plugins/apm/public/components/shared/charts/Legend/index.js deleted file mode 100644 index 601482430b00f..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/Legend/index.js +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { PureComponent } from 'react'; -import styled from 'styled-components'; -import { units, px, fontSizes } from '../../../../style/variables'; -import theme from '@elastic/eui/dist/eui_theme_light.json'; - -const Container = styled.div` - display: flex; - align-items: center; - font-size: ${props => props.fontSize}; - color: ${theme.euiColorDarkShade}; - cursor: ${props => (props.clickable ? 'pointer' : 'initial')}; - opacity: ${props => (props.disabled ? 0.4 : 1)}; - user-select: none; -`; - -export const Indicator = styled.span` - width: ${props => px(props.radius)}; - height: ${props => px(props.radius)}; - margin-right: ${props => px(props.radius / 2)}; - background: ${props => props.color}; - border-radius: 100%; -`; - -export default class Legend extends PureComponent { - render() { - const { - onClick, - text, - color = theme.euiColorVis1, - fontSize = fontSizes.small, - radius = units.minus - 1, - disabled = false, - clickable = false, - indicator, - ...rest - } = this.props; - return ( - - {indicator ? indicator() : } - {text} - - ); - } -} diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Legend/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/charts/Legend/index.tsx new file mode 100644 index 0000000000000..436b020bc9eba --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/Legend/index.tsx @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import theme from '@elastic/eui/dist/eui_theme_light.json'; +import React from 'react'; +import styled from 'styled-components'; +import { fontSizes, px, units } from '../../../../style/variables'; + +export enum Shape { + circle = 'circle', + square = 'square' +} + +interface ContainerProps { + onClick: (e: Event) => void; + fontSize?: string; + clickable: boolean; + disabled: boolean; +} +const Container = styled.div` + display: flex; + align-items: center; + font-size: ${props => props.fontSize}; + color: ${theme.euiColorDarkShade}; + cursor: ${props => (props.clickable ? 'pointer' : 'initial')}; + opacity: ${props => (props.disabled ? 0.4 : 1)}; + user-select: none; +`; + +interface IndicatorProps { + radius: number; + color: string; + shape: Shape; + withMargin: boolean; +} +export const Indicator = styled.span` + width: ${props => px(props.radius)}; + height: ${props => px(props.radius)}; + margin-right: ${props => (props.withMargin ? px(props.radius / 2) : 0)}; + background: ${props => props.color}; + border-radius: ${props => { + return props.shape === Shape.circle ? '100%' : '0'; + }}; +`; + +interface Props { + onClick?: any; + text?: string; + color?: string; + fontSize?: string; + radius?: number; + disabled?: boolean; + clickable?: boolean; + shape?: Shape; + indicator?: () => React.ReactNode; +} + +export const Legend: React.FC = ({ + onClick, + text, + color = theme.euiColorVis1, + fontSize = fontSizes.small, + radius = units.minus - 1, + disabled = false, + clickable = false, + shape = Shape.circle, + indicator, + ...rest +}) => { + return ( + + {indicator ? ( + indicator() + ) : ( + + )} + {text} + + ); +}; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/AgentMarker.js b/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx similarity index 56% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/AgentMarker.js rename to x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx index 8ee23d61fe0eb..ffdbfe6cce7ec 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/AgentMarker.js +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import PropTypes from 'prop-types'; import { EuiToolTip } from '@elastic/eui'; -import Legend from '../Legend'; -import { units, px } from '../../../../style/variables'; -import styled from 'styled-components'; -import { asDuration } from '../../../../utils/formatters'; import theme from '@elastic/eui/dist/eui_theme_light.json'; +import React from 'react'; +import styled from 'styled-components'; +import { px, units } from '../../../../../style/variables'; +import { asDuration } from '../../../../../utils/formatters'; +import { Legend } from '../../Legend'; +import { AgentMark } from '../../../../app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks'; const NameContainer = styled.div` border-bottom: 1px solid ${theme.euiColorMediumShade}; @@ -23,33 +23,25 @@ const TimeContainer = styled.div` padding-top: ${px(units.half)}; `; -export default function AgentMarker({ agentMark, x }) { - const legendWidth = 11; +interface Props { + mark: AgentMark; +} + +export const AgentMarker: React.FC = ({ mark }) => { return ( -
+ <> - {agentMark.name} - {asDuration(agentMark.us)} + {mark.id} + {asDuration(mark.offset)}
} > -
+ ); -} - -AgentMarker.propTypes = { - agentMark: PropTypes.object.isRequired, - x: PropTypes.number.isRequired }; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx b/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx new file mode 100644 index 0000000000000..51368a4fb946d --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiPopover, EuiText } from '@elastic/eui'; +import theme from '@elastic/eui/dist/eui_theme_light.json'; +import React, { useState } from 'react'; +import styled from 'styled-components'; +import { + TRACE_ID, + TRANSACTION_ID +} from '../../../../../../common/elasticsearch_fieldnames'; +import { useUrlParams } from '../../../../../hooks/useUrlParams'; +import { px, unit, units } from '../../../../../style/variables'; +import { asDuration } from '../../../../../utils/formatters'; +import { ErrorMark } from '../../../../app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks'; +import { ErrorDetailLink } from '../../../Links/apm/ErrorDetailLink'; +import { Legend, Shape } from '../../Legend'; + +interface Props { + mark: ErrorMark; +} + +const Popover = styled.div` + max-width: ${px(280)}; +`; + +const TimeLegend = styled(Legend)` + margin-bottom: ${px(unit)}; +`; + +const ErrorLink = styled(ErrorDetailLink)` + display: block; + margin: ${px(units.half)} 0 ${px(units.half)} 0; +`; + +const Button = styled(Legend)` + height: 20px; + display: flex; + align-items: flex-end; +`; + +export const ErrorMarker: React.FC = ({ mark }) => { + const { urlParams } = useUrlParams(); + const [isPopoverOpen, showPopover] = useState(false); + + const togglePopover = () => showPopover(!isPopoverOpen); + + const button = ( +