diff --git a/packages/jaeger-ui/src/components/Monitor/ServicesView/__snapshots__/index.test.js.snap b/packages/jaeger-ui/src/components/Monitor/ServicesView/__snapshots__/index.test.js.snap index fc76e76f70..1cfd5b5abb 100644 --- a/packages/jaeger-ui/src/components/Monitor/ServicesView/__snapshots__/index.test.js.snap +++ b/packages/jaeger-ui/src/components/Monitor/ServicesView/__snapshots__/index.test.js.snap @@ -19,6 +19,7 @@ exports[` ATM snapshot test 1`] = ` ATM snapshot test 1`] = ` View all traces @@ -70,6 +72,7 @@ exports[` ATM snapshot test 1`] = ` ATM snapshot test 1`] = ` "value": 3600000, }, Object { - "label": "Last 2 hour", + "label": "Last 2 hours", "value": 7200000, }, Object { - "label": "Last 6 hour", + "label": "Last 6 hours", "value": 21600000, }, Object { - "label": "Last 12 hour", + "label": "Last 12 hours", "value": 43200000, }, Object { @@ -418,6 +421,7 @@ exports[` ATM snapshot test with no metrics 1`] = ` ATM snapshot test with no metrics 1`] = ` View all traces @@ -460,6 +465,7 @@ exports[` ATM snapshot test with no metrics 1`] = ` ATM snapshot test with no metrics 1`] = ` "value": 3600000, }, Object { - "label": "Last 2 hour", + "label": "Last 2 hours", "value": 7200000, }, Object { - "label": "Last 6 hour", + "label": "Last 6 hours", "value": 21600000, }, Object { - "label": "Last 12 hour", + "label": "Last 12 hours", "value": 43200000, }, Object { @@ -727,6 +733,7 @@ exports[` render one service latency 1`] = ` render one service latency 1`] = ` View all traces @@ -769,6 +777,7 @@ exports[` render one service latency 1`] = ` render one service latency 1`] = ` "value": 3600000, }, Object { - "label": "Last 2 hour", + "label": "Last 2 hours", "value": 7200000, }, Object { - "label": "Last 6 hour", + "label": "Last 6 hours", "value": 21600000, }, Object { - "label": "Last 12 hour", + "label": "Last 12 hours", "value": 43200000, }, Object { diff --git a/packages/jaeger-ui/src/components/Monitor/ServicesView/index.test.js b/packages/jaeger-ui/src/components/Monitor/ServicesView/index.test.js index c5aa74801c..4d5cac7553 100644 --- a/packages/jaeger-ui/src/components/Monitor/ServicesView/index.test.js +++ b/packages/jaeger-ui/src/components/Monitor/ServicesView/index.test.js @@ -20,6 +20,7 @@ import { mapDispatchToProps, getLoopbackInterval, yAxisTickFormat, + timeFrameOptions, } from '.'; import LoadingIndicator from '../../common/LoadingIndicator'; import MonitorATMEmptyState from '../EmptyState'; @@ -30,6 +31,7 @@ import { serviceOpsMetrics, serviceMetricsWithOneServiceLatency, } from '../../../reducers/metrics.mock'; +import * as track from './index.track'; const state = { services: {}, @@ -40,12 +42,14 @@ const state = { const props = mapStateToProps(state); Date.now = jest.fn(() => 1487076708000); // Tue, 14 Feb 2017 12:51:48 GMT' +jest.mock('lodash/debounce', () => fn => fn); describe('', () => { let wrapper; const mockFetchServices = jest.fn(); const mockFetchAllServiceMetrics = jest.fn(); const mockFetchAggregatedServiceMetrics = jest.fn(); + beforeAll(() => { Date.now = jest.fn(() => 1466424490000); }); @@ -299,6 +303,43 @@ describe('', () => { .prop('error') ).not.toBeNull(); }); + + it('Should track all events', () => { + const trackSelectServiceSpy = jest.spyOn(track, 'trackSelectService'); + const trackViewAllTracesSpy = jest.spyOn(track, 'trackViewAllTraces'); + const trackSelectTimeframeSpy = jest.spyOn(track, 'trackSelectTimeframe'); + const trackSearchOperationSpy = jest.spyOn(track, 'trackSearchOperation'); + + const newValue = 'newValue'; + const [timeFrameOption] = timeFrameOptions; + + wrapper.setProps({ + metrics: { ...originInitialState, serviceOpsMetrics }, + }); + + wrapper.find('Search').simulate('change', { target: { value: newValue } }); + expect(trackSearchOperationSpy).toHaveBeenCalledWith(newValue); + + wrapper + .find('Field') + .first() + .simulate('change', null, newValue); + expect(trackSelectServiceSpy).toHaveBeenCalledWith(newValue); + + wrapper + .find('Field') + .last() + .simulate('change', null, timeFrameOption.value); + expect(trackSelectTimeframeSpy).toHaveBeenCalledWith(timeFrameOption.label); + + wrapper.find({ children: 'View all traces' }).simulate('click'); + expect(trackViewAllTracesSpy).toHaveBeenCalled(); + + trackSelectServiceSpy.mockReset(); + trackViewAllTracesSpy.mockReset(); + trackSelectTimeframeSpy.mockReset(); + trackSearchOperationSpy.mockReset(); + }); }); describe(' on page switch', () => { diff --git a/packages/jaeger-ui/src/components/Monitor/ServicesView/index.track.test.js b/packages/jaeger-ui/src/components/Monitor/ServicesView/index.track.test.js new file mode 100644 index 0000000000..515a7a58f8 --- /dev/null +++ b/packages/jaeger-ui/src/components/Monitor/ServicesView/index.track.test.js @@ -0,0 +1,60 @@ +// Copyright (c) 2022 Uber Technologies, Inc. +// +// Licensed 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 * as trackingUtils from '../../../utils/tracking'; +import { + CATEGORY_SEARCH_OPERATION, + CATEGORY_SELECT_SERVICE, + CATEGORY_SELECT_TIMEFRAME, + CATEGORY_VIEW_ALL_TRACES, + trackSelectService, + trackSelectTimeframe, + trackViewAllTraces, + trackSearchOperation, +} from './index.track'; + +describe('ServicesView tracking', () => { + let trackEvent; + + beforeAll(() => { + trackEvent = jest.spyOn(trackingUtils, 'trackEvent').mockImplementation(); + }); + + beforeEach(() => { + trackEvent.mockClear(); + }); + + it('trackViewAllTraces calls trackEvent with the match category and show action', () => { + trackViewAllTraces(); + expect(trackEvent).toHaveBeenCalledWith(CATEGORY_VIEW_ALL_TRACES, expect.any(String)); + }); + + it('trackSelectService calls trackEvent with the match category and show action', () => { + const serviceName = 'service-name'; + trackSelectService(serviceName); + expect(trackEvent).toHaveBeenCalledWith(CATEGORY_SELECT_SERVICE, serviceName); + }); + + it('trackSelectTimeframe calls trackEvent with the match category and show action', () => { + const timeframe = 'some-timeframe'; + trackSelectTimeframe(timeframe); + expect(trackEvent).toHaveBeenCalledWith(CATEGORY_SELECT_TIMEFRAME, timeframe); + }); + + it('trackSearchOperation calls trackEvent with the match category and show action', async () => { + const searchQuery = 'name'; + trackSearchOperation(searchQuery); + expect(trackEvent).toHaveBeenCalledWith(CATEGORY_SEARCH_OPERATION, searchQuery); + }); +}); diff --git a/packages/jaeger-ui/src/components/Monitor/ServicesView/index.track.tsx b/packages/jaeger-ui/src/components/Monitor/ServicesView/index.track.tsx new file mode 100644 index 0000000000..0ae66ad4b9 --- /dev/null +++ b/packages/jaeger-ui/src/components/Monitor/ServicesView/index.track.tsx @@ -0,0 +1,28 @@ +// Copyright (c) 2022 The Jaeger Authors. +// +// Licensed 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 { trackEvent } from '../../../utils/tracking'; + +const SPM_CATEGORY_BASE = 'jaeger/ux/trace/spm'; + +export const CATEGORY_VIEW_ALL_TRACES = `${SPM_CATEGORY_BASE}/view-all-traces`; +export const CATEGORY_SELECT_SERVICE = `${SPM_CATEGORY_BASE}/select-service`; +export const CATEGORY_SELECT_TIMEFRAME = `${SPM_CATEGORY_BASE}/select-timeframe`; +export const CATEGORY_SEARCH_OPERATION = `${SPM_CATEGORY_BASE}/search-operation`; + +export const trackViewAllTraces = () => trackEvent(CATEGORY_VIEW_ALL_TRACES, 'click'); +export const trackSelectService = (service: string) => trackEvent(CATEGORY_SELECT_SERVICE, service); +export const trackSelectTimeframe = (timeframe: string) => trackEvent(CATEGORY_SELECT_TIMEFRAME, timeframe); +export const trackSearchOperation = (searchQuery: string) => + trackEvent(CATEGORY_SEARCH_OPERATION, searchQuery); diff --git a/packages/jaeger-ui/src/components/Monitor/ServicesView/index.tsx b/packages/jaeger-ui/src/components/Monitor/ServicesView/index.tsx index 88fdc1c402..c1ac3f8f71 100644 --- a/packages/jaeger-ui/src/components/Monitor/ServicesView/index.tsx +++ b/packages/jaeger-ui/src/components/Monitor/ServicesView/index.tsx @@ -15,6 +15,7 @@ import * as React from 'react'; import { Row, Col, Input, Alert } from 'antd'; import { ActionFunction, Action } from 'redux-actions'; +import _debounce from 'lodash/debounce'; import _isEqual from 'lodash/isEqual'; import _isEmpty from 'lodash/isEmpty'; // @ts-ignore @@ -44,6 +45,12 @@ import { convertToTimeUnit, convertTimeUnitToShortTerm, getSuitableTimeUnit } fr import './index.css'; import { getConfigValue } from '../../../utils/config/get-config'; +import { + trackSearchOperation, + trackSelectService, + trackSelectTimeframe, + trackViewAllTraces, +} from './index.track'; type StateType = { graphWidth: number; @@ -68,6 +75,8 @@ type TDispatchProps = { fetchAllServiceMetrics: (serviceName: string, query: MetricsAPIQueryParams) => void; }; +const trackSearchOperationDebounced = _debounce(searchQuery => trackSearchOperation(searchQuery), 1000); + const Search = Input.Search; const AdaptedVirtualSelect = reduxFormFieldAdapter({ @@ -77,11 +86,11 @@ const AdaptedVirtualSelect = reduxFormFieldAdapter({ const serviceFormSelector = formValueSelector('serviceForm'); const oneHourInMilliSeconds = 3600000; -const timeFrameOptions = [ +export const timeFrameOptions = [ { label: 'Last Hour', value: oneHourInMilliSeconds }, - { label: 'Last 2 hour', value: 2 * oneHourInMilliSeconds }, - { label: 'Last 6 hour', value: 6 * oneHourInMilliSeconds }, - { label: 'Last 12 hour', value: 12 * oneHourInMilliSeconds }, + { label: 'Last 2 hours', value: 2 * oneHourInMilliSeconds }, + { label: 'Last 6 hours', value: 6 * oneHourInMilliSeconds }, + { label: 'Last 12 hours', value: 12 * oneHourInMilliSeconds }, { label: 'Last 24 hours', value: 24 * oneHourInMilliSeconds }, { label: 'Last 2 days', value: 48 * oneHourInMilliSeconds }, ]; @@ -258,6 +267,7 @@ export class MonitorATMServicesViewImpl extends React.PureComponent

Choose service

trackSelectService(newValue)} name="service" component={AdaptedVirtualSelect} placeholder="Select A Service" @@ -284,6 +294,7 @@ export class MonitorATMServicesViewImpl extends React.PureComponent View all traces
@@ -294,6 +305,10 @@ export class MonitorATMServicesViewImpl extends React.PureComponent { + const { label } = timeFrameOptions.find(option => option.value === value)!; + trackSelectTimeframe(label); + }} props={{ className: 'select-a-timeframe-input', defaultValue: timeFrameOptions[0], @@ -379,6 +394,8 @@ export class MonitorATMServicesViewImpl extends React.PureComponent diff --git a/packages/jaeger-ui/src/components/Monitor/ServicesView/operationDetailsTable/__snapshots__/index.test.js.snap b/packages/jaeger-ui/src/components/Monitor/ServicesView/operationDetailsTable/__snapshots__/index.test.js.snap index 3a02d40416..0e066c9135 100644 --- a/packages/jaeger-ui/src/components/Monitor/ServicesView/operationDetailsTable/__snapshots__/index.test.js.snap +++ b/packages/jaeger-ui/src/components/Monitor/ServicesView/operationDetailsTable/__snapshots__/index.test.js.snap @@ -77,7 +77,8 @@ exports[` Table rendered successfully 1`] = ` } } > - Impact   + Impact +   Table rendered successfully 1`] = ` indentSize={20} loading={false} locale={Object {}} + onChange={[Function]} onRow={[Function]} pagination={ Object { @@ -365,7 +367,8 @@ exports[` render No data table 1`] = ` } } > - Impact   + Impact +   render P95 latency with more than 2 decimal pla } } > - Impact   + Impact +   render error rate with more than 2 decimal plac } } > - Impact   + Impact +   render lower than 0.1 P95 latency 1`] = ` } } > - Impact   + Impact +   render lower than 0.1 error rate 1`] = ` } } > - Impact   + Impact +   render lower than 0.1 request rate value 1`] = } } > - Impact   + Impact +   render request rate number with more than 2 dec } } > - Impact   + Impact +   render some values in the table 1`] = ` } } > - Impact   + Impact +   test column render function 1`] = ` } } > - Impact   + Impact +   ', () => { .text() ).toBe(''); }); + + it('Should track all events', async () => { + const trackSortOperationsSpy = jest.spyOn(track, 'trackSortOperations'); + const trackViewTracesSpy = jest.spyOn(track, 'trackViewTraces'); + const recordIndex = 0; + + wrapper.setProps({ ...props, loading: false, data: serviceOpsMetrics }); + + // Hover on first line in the t able and display the button + wrapper + .find('.ant-table-row.table-row') + .at(recordIndex) + .simulate('mouseenter'); + wrapper + .find({ children: 'View traces' }) + .first() + .simulate('click'); + + expect(trackViewTracesSpy).toHaveBeenCalledWith(serviceOpsMetrics[recordIndex].name); + + wrapper + .find('.ant-table-column-sorter-down.off') + .first() + .simulate('click'); + expect(trackSortOperationsSpy).toHaveBeenCalledWith('Name'); + + wrapper + .find('.ant-table-column-sorter-down.off') + .last() + .simulate('click'); + expect(trackSortOperationsSpy).toHaveBeenCalledWith('Impact'); + + trackSortOperationsSpy.mockReset(); + trackViewTracesSpy.mockReset(); + }); }); diff --git a/packages/jaeger-ui/src/components/Monitor/ServicesView/operationDetailsTable/index.track.test.js b/packages/jaeger-ui/src/components/Monitor/ServicesView/operationDetailsTable/index.track.test.js new file mode 100644 index 0000000000..8ac94d6412 --- /dev/null +++ b/packages/jaeger-ui/src/components/Monitor/ServicesView/operationDetailsTable/index.track.test.js @@ -0,0 +1,45 @@ +// Copyright (c) 2022 Uber Technologies, Inc. +// +// Licensed 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 * as trackingUtils from '../../../../utils/tracking'; +import { + CATEGORY_VIEW_TRACES, + CATEGORY_SORT_OPERATIONS, + trackSortOperations, + trackViewTraces, +} from './index.track'; + +describe('operationDetailsTable tracking', () => { + let trackEvent; + + beforeAll(() => { + trackEvent = jest.spyOn(trackingUtils, 'trackEvent').mockImplementation(); + }); + + beforeEach(() => { + trackEvent.mockClear(); + }); + + it('trackViewTraces calls trackEvent with the match category and show action', () => { + const traceName = 'trace-name'; + trackViewTraces(traceName); + expect(trackEvent).toHaveBeenCalledWith(CATEGORY_VIEW_TRACES, traceName); + }); + + it('trackSortOperations calls trackEvent with the match category and show action', () => { + const sortColumn = 'some-column'; + trackSortOperations(sortColumn); + expect(trackEvent).toHaveBeenCalledWith(CATEGORY_SORT_OPERATIONS, sortColumn); + }); +}); diff --git a/packages/jaeger-ui/src/components/Monitor/ServicesView/operationDetailsTable/index.track.tsx b/packages/jaeger-ui/src/components/Monitor/ServicesView/operationDetailsTable/index.track.tsx new file mode 100644 index 0000000000..080e2698b3 --- /dev/null +++ b/packages/jaeger-ui/src/components/Monitor/ServicesView/operationDetailsTable/index.track.tsx @@ -0,0 +1,22 @@ +// Copyright (c) 2022 The Jaeger Authors. +// +// Licensed 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 { trackEvent } from '../../../../utils/tracking'; + +const SPM_CATEGORY_BASE = 'jaeger/ux/trace/spm'; +export const CATEGORY_VIEW_TRACES = `${SPM_CATEGORY_BASE}/view-traces`; +export const CATEGORY_SORT_OPERATIONS = `${SPM_CATEGORY_BASE}/sort-operations`; + +export const trackViewTraces = (name: string) => trackEvent(CATEGORY_VIEW_TRACES, name); +export const trackSortOperations = (columnName: string) => trackEvent(CATEGORY_SORT_OPERATIONS, columnName); diff --git a/packages/jaeger-ui/src/components/Monitor/ServicesView/operationDetailsTable/index.tsx b/packages/jaeger-ui/src/components/Monitor/ServicesView/operationDetailsTable/index.tsx index 7904450a4a..c4ca3fab69 100644 --- a/packages/jaeger-ui/src/components/Monitor/ServicesView/operationDetailsTable/index.tsx +++ b/packages/jaeger-ui/src/components/Monitor/ServicesView/operationDetailsTable/index.tsx @@ -13,6 +13,8 @@ // limitations under the License. import * as React from 'react'; +import isEqual from 'lodash/isEqual'; +import { SorterResult } from 'antd/es/table'; import { Row, Table, Progress, Button, Icon, Tooltip } from 'antd'; import REDGraph from './opsGraph'; import LoadingIndicator from '../../../common/LoadingIndicator'; @@ -21,6 +23,7 @@ import prefixUrl from '../../../../utils/prefix-url'; import './index.css'; import { convertTimeUnitToShortTerm, convertToTimeUnit, getSuitableTimeUnit } from '../../../../utils/date'; +import { trackSortOperations, trackViewTraces } from './index.track'; type TProps = { data: ServiceOpsMetrics[] | undefined; @@ -33,8 +36,17 @@ type TProps = { type TState = { hoveredRowKey: number; + tableSorting: Pick, 'columnKey' | 'order'>; }; +const tableTitles = new Map([ + ['name', 'Name'], + ['latency', 'P95 Latency'], + ['requests', 'Request rate'], + ['errRates', 'Error rate'], + ['impact', 'Impact'], +]); + function formatValue(value: number) { if (value < 0.1) { return `< 0.1`; @@ -51,8 +63,12 @@ function formatTimeValue(value: number) { return `${formattedTime}${convertTimeUnitToShortTerm(timeUnit)}`; } export class OperationTableDetails extends React.PureComponent { - state = { + state: TState = { hoveredRowKey: -1, + tableSorting: { + order: 'descend', + columnKey: 'impact', + }, }; render() { @@ -68,14 +84,14 @@ export class OperationTableDetails extends React.PureComponent { const columnConfig = [ { - title: 'Name', + title: tableTitles.get('name'), className: 'header-item', dataIndex: 'name', key: 'name', sorter: (a: ServiceOpsMetrics, b: ServiceOpsMetrics) => a.name.localeCompare(b.name), }, { - title: 'P95 Latency', + title: tableTitles.get('latency'), className: 'header-item', dataIndex: 'latency', key: 'latency', @@ -96,7 +112,7 @@ export class OperationTableDetails extends React.PureComponent { ), }, { - title: 'Request rate', + title: tableTitles.get('requests'), className: 'header-item', dataIndex: 'requests', key: 'requests', @@ -117,7 +133,7 @@ export class OperationTableDetails extends React.PureComponent { ), }, { - title: 'Error rate', + title: tableTitles.get('errRates'), className: 'header-item', dataIndex: 'errRates', key: 'errRates', @@ -142,7 +158,7 @@ export class OperationTableDetails extends React.PureComponent { title: (
- Impact   + {tableTitles.get('impact')}   { )}&service=${serviceName}&start=${endTime - lookback}000` )} target="blank" + onClick={() => trackViewTraces(row.name)} > View traces @@ -216,6 +233,14 @@ export class OperationTableDetails extends React.PureComponent { }, }; }} + onChange={(pagination, filters, { columnKey, order }) => { + if (!isEqual({ columnKey, order }, this.state.tableSorting)) { + const clickedColumn = tableTitles.get(columnKey || this.state.tableSorting.columnKey); + + trackSortOperations(clickedColumn!); + this.setState({ tableSorting: { columnKey, order } }); + } + }} /> );