diff --git a/package.json b/package.json index 2ff86f238e..a8e6e344b1 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,6 @@ "flow-bin": "^0.36.0", "fuzzy": "^0.1.1", "global": "^4.3.0", - "immutable": "^3.8.1", "is-promise": "^2.1.0", "isomorphic-fetch": "^2.2.1", "json-markup": "^1.0.0", @@ -49,7 +48,6 @@ "react-dom": "^15.5.0", "react-ga": "^2.1.2", "react-helmet": "^3.1.0", - "react-immutable-proptypes": "^2.1.0", "react-metrics": "^2.2.3", "react-redux": "^4.4.5", "react-router": "^2.7.0", diff --git a/src/components/DependencyGraph/index.js b/src/components/DependencyGraph/index.js index 468a1245c7..96e7f2a82d 100644 --- a/src/components/DependencyGraph/index.js +++ b/src/components/DependencyGraph/index.js @@ -62,7 +62,6 @@ export default class DependencyGraphPage extends Component { render() { const { nodes, links, error, dependencies, loading } = this.props; const { graphType } = this.state; - const serviceCalls = dependencies.toJS(); if (loading) { return (
@@ -86,7 +85,7 @@ export default class DependencyGraphPage extends Component { const GRAPH_TYPE_OPTIONS = [{ type: 'FORCE_DIRECTED', name: 'Force Directed Graph' }]; - if (serviceCalls.length <= 100) { + if (dependencies.length <= 100) { GRAPH_TYPE_OPTIONS.push({ type: 'DAG', name: 'DAG' }); } return ( @@ -94,8 +93,9 @@ export default class DependencyGraphPage extends Component { {GRAPH_TYPE_OPTIONS.map(option => this.handleGraphTypeChange(option.type)} /> )} @@ -112,7 +112,7 @@ export default class DependencyGraphPage extends Component { }} > {graphType === 'FORCE_DIRECTED' && } - {graphType === 'DAG' && } + {graphType === 'DAG' && }
); @@ -121,17 +121,14 @@ export default class DependencyGraphPage extends Component { // export connected component separately function mapStateToProps(state) { - const dependencies = state.dependencies.get('dependencies'); - let nodes; + const { dependencies, error, loading } = state.dependencies; let links; - if (dependencies && dependencies.size > 0) { - const nodesAndLinks = formatDependenciesAsNodesAndLinks({ dependencies }); - nodes = nodesAndLinks.nodes; - links = nodesAndLinks.links; + let nodes; + if (dependencies && dependencies.length > 0) { + const formatted = formatDependenciesAsNodesAndLinks({ dependencies }); + links = formatted.links; + nodes = formatted.nodes; } - const error = state.dependencies.get('error'); - const loading = state.dependencies.get('loading'); - return { loading, error, nodes, links, dependencies }; } diff --git a/src/components/SearchTracePage/TraceSearchForm.js b/src/components/SearchTracePage/TraceSearchForm.js index cf05638342..18ed3c658b 100644 --- a/src/components/SearchTracePage/TraceSearchForm.js +++ b/src/components/SearchTracePage/TraceSearchForm.js @@ -92,7 +92,7 @@ export function TraceSearchFormComponent(props) { name="service" component={SearchDropdownInput} className="ui dropdown" - items={services.concat({ name: '-' }).map(s => ({ text: s.name, value: s.name }))} + items={services.concat({ name: '-' }).map(s => ({ text: s.name, value: s.name, key: s.name }))} /> @@ -102,7 +102,7 @@ export function TraceSearchFormComponent(props) { name="operation" component={SearchDropdownInput} className="ui dropdown" - items={operationsForService.concat('all').map(op => ({ text: op, value: op }))} + items={operationsForService.concat('all').map(op => ({ text: op, value: op, key: op }))} /> } @@ -190,7 +190,12 @@ export function TraceSearchFormComponent(props) { TraceSearchFormComponent.propTypes = { handleSubmit: PropTypes.func, submitting: PropTypes.bool, - services: PropTypes.arrayOf(PropTypes.string), + services: PropTypes.arrayOf( + PropTypes.shape({ + name: PropTypes.string, + operations: PropTypes.arrayOf(PropTypes.string), + }) + ), selectedService: PropTypes.string, selectedLookback: PropTypes.string, }; diff --git a/src/components/SearchTracePage/TraceSearchResult.js b/src/components/SearchTracePage/TraceSearchResult.js index f0fa6fd20b..aa24961941 100644 --- a/src/components/SearchTracePage/TraceSearchResult.js +++ b/src/components/SearchTracePage/TraceSearchResult.js @@ -55,7 +55,7 @@ export default function TraceSearchResult({ trace, durationPercent = 100 }) { {numberOfSpans} span{numberOfSpans > 1 && 's'} - {numberOfErredSpans && + {Boolean(numberOfErredSpans) && {numberOfErredSpans} error{numberOfErredSpans > 1 && 's'} } @@ -63,7 +63,7 @@ export default function TraceSearchResult({ trace, durationPercent = 100 }) {
{sortBy(services, s => s.name).map(service =>
- +
)}
diff --git a/src/components/SearchTracePage/TraceSearchResult.test.js b/src/components/SearchTracePage/TraceSearchResult.test.js index 98b318f7b5..ee854d0921 100644 --- a/src/components/SearchTracePage/TraceSearchResult.test.js +++ b/src/components/SearchTracePage/TraceSearchResult.test.js @@ -29,7 +29,7 @@ const testTraceProps = { services: [ { name: 'Service A', - numberOfApperancesInTrace: 2, + numberOfSpans: 2, percentOfTrace: 50, }, ], diff --git a/src/components/SearchTracePage/TraceServiceTag.js b/src/components/SearchTracePage/TraceServiceTag.js index c6c96de49c..b9f4d96258 100644 --- a/src/components/SearchTracePage/TraceServiceTag.js +++ b/src/components/SearchTracePage/TraceServiceTag.js @@ -23,10 +23,10 @@ import React from 'react'; import colorGenerator from '../../utils/color-generator'; export default function TraceServiceTag({ service }) { - const { name, numberOfApperancesInTrace } = service; + const { name, numberOfSpans } = service; return (
- {name} ({numberOfApperancesInTrace}) + {name} ({numberOfSpans})
); } @@ -34,6 +34,6 @@ export default function TraceServiceTag({ service }) { TraceServiceTag.propTypes = { service: PropTypes.shape({ name: PropTypes.string.isRequired, - numberOfApperancesInTrace: PropTypes.number.isRequired, + numberOfSpans: PropTypes.number.isRequired, }).isRequired, }; diff --git a/src/components/SearchTracePage/TraceServiceTag.test.js b/src/components/SearchTracePage/TraceServiceTag.test.js index 8b9b10989a..a9e241a8bd 100644 --- a/src/components/SearchTracePage/TraceServiceTag.test.js +++ b/src/components/SearchTracePage/TraceServiceTag.test.js @@ -28,7 +28,7 @@ it(' tests', () => { ); diff --git a/src/components/SearchTracePage/index.js b/src/components/SearchTracePage/index.js index aef24cf6a1..b4f0da034e 100644 --- a/src/components/SearchTracePage/index.js +++ b/src/components/SearchTracePage/index.js @@ -18,6 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. +import _values from 'lodash/values'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { bindActionCreators } from 'redux'; @@ -25,22 +26,15 @@ import { connect } from 'react-redux'; import { Field, reduxForm, formValueSelector } from 'redux-form'; import { Link } from 'react-router'; import { Sticky } from 'react-sticky'; -import * as jaegerApiActions from '../../actions/jaeger-api'; import JaegerLogo from '../../img/jaeger-logo.svg'; +import * as jaegerApiActions from '../../actions/jaeger-api'; import TraceSearchForm from './TraceSearchForm'; import TraceSearchResult from './TraceSearchResult'; import TraceResultsScatterPlot from './TraceResultsScatterPlot'; -import { - transformTraceResultsSelector, - getSortedTraceResults, - LONGEST_FIRST, - SHORTEST_FIRST, - MOST_SPANS, - LEAST_SPANS, - MOST_RECENT, -} from '../../selectors/search'; +import * as orderBy from '../../model/order-by'; +import { sortTraces, getTraceSummaries } from '../../model/search'; import { getPercentageOfDuration } from '../../utils/date'; import getLastXformCacher from '../../utils/get-last-xform-cacher'; @@ -52,18 +46,18 @@ let TraceResultsFilterForm = () =>
- - - - - + + + + +
; TraceResultsFilterForm = reduxForm({ form: 'traceResultsFilters', initialValues: { - sortBy: MOST_RECENT, + sortBy: orderBy.MOST_RECENT, }, })(TraceResultsFilterForm); const traceResultsFiltersFormSelector = formValueSelector('traceResultsFilters'); @@ -194,13 +188,13 @@ SearchTracePage.propTypes = { }; const stateTraceXformer = getLastXformCacher(stateTrace => { - const { traces: traceMap, loading, error: traceError } = stateTrace.toJS(); - const traces = Object.keys(traceMap).map(traceID => traceMap[traceID]); - return { tracesSrc: { traces }, loading, traceError }; + const { traces: traceMap, loading, error: traceError } = stateTrace; + const { traces, maxDuration } = getTraceSummaries(_values(traceMap)); + return { traces, maxDuration, loading, traceError }; }); const stateServicesXformer = getLastXformCacher(stateServices => { - const { services: serviceList, operationsForService: opsBySvc, error: serviceError } = stateServices.toJS(); + const { services: serviceList, operationsForService: opsBySvc, error: serviceError } = stateServices; const services = serviceList.map(name => ({ name, operations: opsBySvc[name] || [], @@ -211,18 +205,17 @@ const stateServicesXformer = getLastXformCacher(stateServices => { function mapStateToProps(state) { const query = state.routing.locationBeforeTransitions.query; const isHomepage = !Object.keys(query).length; - const { tracesSrc, loading, traceError } = stateTraceXformer(state.trace); - const { traces, maxDuration } = transformTraceResultsSelector(tracesSrc); + const { traces, maxDuration, loading, traceError } = stateTraceXformer(state.trace); const { services, serviceError } = stateServicesXformer(state.services); - const sortBy = traceResultsFiltersFormSelector(state, 'sortBy'); - const traceResultsSorted = getSortedTraceResults(traces, sortBy); const errorMessage = serviceError || traceError ? `${serviceError || ''} ${traceError || ''}` : ''; + const sortBy = traceResultsFiltersFormSelector(state, 'sortBy'); + sortTraces(traces, sortBy); return { isHomepage, sortTracesBy: sortBy, - traceResults: traceResultsSorted, - numberOfTraceResults: traceResultsSorted.length, + traceResults: traces, + numberOfTraceResults: traces.length, maxTraceDuration: maxDuration, urlQueryParams: query, services, @@ -233,7 +226,6 @@ function mapStateToProps(state) { function mapDispatchToProps(dispatch) { const { searchTraces, fetchServices } = bindActionCreators(jaegerApiActions, dispatch); - return { searchTraces, fetchServices, diff --git a/src/components/TracePage/TraceTimelineViewer/SpanDetail.js b/src/components/TracePage/TraceTimelineViewer/SpanDetail.js index 8ed2a190aa..d34d383d45 100644 --- a/src/components/TracePage/TraceTimelineViewer/SpanDetail.js +++ b/src/components/TracePage/TraceTimelineViewer/SpanDetail.js @@ -44,7 +44,7 @@ function CollapsePanel(props) { ); } CollapsePanel.propTypes = { - header: PropTypes.element.isRequired, + header: PropTypes.node.isRequired, onToggleOpen: PropTypes.func.isRequired, children: PropTypes.element.isRequired, open: PropTypes.bool.isRequired, diff --git a/src/components/TracePage/index.js b/src/components/TracePage/index.js index 1c2f77c8fa..891f792b0d 100644 --- a/src/components/TracePage/index.js +++ b/src/components/TracePage/index.js @@ -168,18 +168,13 @@ export default class TracePage extends Component { // export connected component separately function mapStateToProps(state, ownProps) { - const { params: { id } } = ownProps; - - let trace = state.trace.getIn(['traces', id]); + const { id } = ownProps.params; + let trace = state.trace.traces[id]; if (trace && !(trace instanceof Error)) { - trace = trace.toJS(); trace = dropEmptyStartTimeSpans(trace); trace = hydrateSpansWithProcesses(trace); } - - const loading = state.trace.get('loading'); - - return { id, loading, trace }; + return { id, trace, loading: state.trace.loading }; } function mapDispatchToProps(dispatch) { diff --git a/src/reducers/index.test.js b/src/model/order-by.js similarity index 83% rename from src/reducers/index.test.js rename to src/model/order-by.js index 5948776c06..84892fc34c 100644 --- a/src/reducers/index.test.js +++ b/src/model/order-by.js @@ -18,9 +18,8 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -import jaegerReducers from './index'; -import traceReducer from './trace'; - -it('jaegerReducers should contain the trace reducer', () => { - expect(jaegerReducers.trace).toBe(traceReducer); -}); +export const MOST_RECENT = 'MOST_RECENT'; +export const LONGEST_FIRST = 'LONGEST_FIRST'; +export const SHORTEST_FIRST = 'SHORTEST_FIRST'; +export const MOST_SPANS = 'MOST_SPANS'; +export const LEAST_SPANS = 'LEAST_SPANS'; diff --git a/src/model/search.js b/src/model/search.js new file mode 100644 index 0000000000..790f78e6b4 --- /dev/null +++ b/src/model/search.js @@ -0,0 +1,116 @@ +// @flow + +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import _map from 'lodash/map'; +import _values from 'lodash/values'; + +import { LEAST_SPANS, LONGEST_FIRST, MOST_RECENT, MOST_SPANS, SHORTEST_FIRST } from './order-by'; +import type { Trace } from '../types'; +import type { TraceSummaries, TraceSummary } from '../types/search'; + +const isErrorTag = ({ key, value }) => key === 'error' && (value === true || value === 'true'); + +/** + * Transforms a trace from the HTTP response to the data structure needed by the search page. Note: exported + * for unit tests. + * + * @param {Trace} trace Trace data in the format sent over the wire. + * @return {TraceSummary} Summary of the trace data for use in the search results. + */ +export function getTraceSummary(trace: Trace): TraceSummary { + const { processes, spans, traceID } = trace; + + let traceName = ''; + let minTs = Number.MAX_SAFE_INTEGER; + let maxTs = Number.MIN_SAFE_INTEGER; + let numErrorSpans = 0; + // serviceName -> { name, numberOfSpans } + const serviceMap = {}; + + for (let i = 0; i < spans.length; i++) { + const { duration, processID, spanID, startTime, tags } = spans[i]; + // time bounds of trace + minTs = minTs > startTime ? startTime : minTs; + maxTs = maxTs < startTime + duration ? startTime + duration : maxTs; + // number of error tags + if (tags.some(isErrorTag)) { + numErrorSpans += 1; + } + // number of span per service + const { serviceName } = processes[processID]; + let svcData = serviceMap[serviceName]; + if (svcData) { + svcData.numberOfSpans += 1; + } else { + svcData = { + name: serviceName, + numberOfSpans: 1, + }; + serviceMap[serviceName] = svcData; + } + // trace name + if (spanID === traceID) { + const { operationName } = spans[i]; + traceName = `${svcData.name}: ${operationName}`; + } + } + return { + traceName, + traceID, + duration: (maxTs - minTs) / 1000, + numberOfErredSpans: numErrorSpans, + numberOfSpans: spans.length, + services: _values(serviceMap), + timestamp: minTs / 1000, + }; +} + +/** + * Transforms `Trace` values into `TraceSummary` values and finds the max duration of the traces. + * + * @param {Trace} _traces The trace data in the format from the HTTP request. + * @return {TraceSummaries} The `{ traces, maxDuration }` value. + */ +export function getTraceSummaries(_traces: Trace[]): TraceSummaries { + const traces = _traces.map(getTraceSummary); + const maxDuration = Math.max(..._map(traces, 'duration')); + return { maxDuration, traces }; +} + +const comparators = { + [MOST_RECENT]: (a, b) => +(b.timestamp > a.timestamp) || +(a.timestamp === b.timestamp) - 1, + [SHORTEST_FIRST]: (a, b) => +(a.duration > b.duration) || +(a.duration === b.duration) - 1, + [LONGEST_FIRST]: (a, b) => +(b.duration > a.duration) || +(a.duration === b.duration) - 1, + [MOST_SPANS]: (a, b) => +(b.numberOfSpans > a.numberOfSpans) || +(a.numberOfSpans === b.numberOfSpans) - 1, + [LEAST_SPANS]: (a, b) => +(a.numberOfSpans > b.numberOfSpans) || +(a.numberOfSpans === b.numberOfSpans) - 1, +}; + +/** + * Sorts `TraceSummary[]`, in place. + * + * @param {TraceSummary[]} traces The `TraceSummary` array to sort. + * @param {string} sortBy A sort specification, see ./order-by.js. + */ +export function sortTraces(traces: TraceSummary[], sortBy: string) { + const comparator = comparators[sortBy] || comparators[LONGEST_FIRST]; + traces.sort(comparator); +} diff --git a/src/model/search.test.js b/src/model/search.test.js new file mode 100644 index 0000000000..ec301b7dae --- /dev/null +++ b/src/model/search.test.js @@ -0,0 +1,134 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import _maxBy from 'lodash/maxBy'; +import _minBy from 'lodash/minBy'; + +import * as orderBy from './order-by'; +import { getTraceSummaries, getTraceSummary, sortTraces } from './search'; +import traceGenerator from '../demo/trace-generators'; + +describe('getTraceSummary()', () => { + let trace; + let summary; + + beforeEach(() => { + trace = traceGenerator.trace({ numberOfSpans: 2 }); + summary = getTraceSummary(trace); + }); + + it('derives duration, timestamp and numberOfSpans', () => { + expect(summary.numberOfSpans).toBe(trace.spans.length); + expect(summary.duration).toBe(trace.duration / 1000); + expect(summary.timestamp).toBe(Math.floor(trace.timestamp / 1000)); + }); + + it('handles error spans', () => { + const errorTag = { key: 'error', value: true }; + expect(summary.numberOfErredSpans).toBe(0); + trace.spans[0].tags.push(errorTag); + expect(getTraceSummary(trace).numberOfErredSpans).toBe(1); + trace.spans[1].tags.push(errorTag); + expect(getTraceSummary(trace).numberOfErredSpans).toBe(2); + }); + + it('generates the traceName', () => { + trace = { + traceID: 'main-id', + spans: [ + { + traceID: 'main-id', + processID: 'pid0', + spanID: 'main-id', + operationName: 'op0', + startTime: 1502221240933000, + duration: 236857, + tags: [], + }, + { + traceID: 'main-id', + processID: 'pid1', + spanID: 'span-child', + operationName: 'op1', + startTime: 1502221241144382, + duration: 25305, + tags: [], + }, + ], + duration: 236857, + timestamp: 1502221240933000, + processes: { + pid0: { + processID: 'pid0', + serviceName: 'serviceA', + tags: [], + }, + pid1: { + processID: 'pid1', + serviceName: 'serviceB', + tags: [], + }, + }, + }; + const { traceName } = getTraceSummary(trace); + expect(traceName).toBe('serviceA: op0'); + }); + + xit('derives services summations', () => {}); +}); + +describe('getTraceSummaries()', () => { + it('finds the max duration', () => { + const traces = [traceGenerator.trace({}), traceGenerator.trace({})]; + const maxDuration = _maxBy(traces, 'duration').duration / 1000; + expect(getTraceSummaries(traces).maxDuration).toBe(maxDuration); + }); +}); + +describe('sortTraces()', () => { + const idMinSpans = 4; + const idMaxSpans = 2; + const rawTraces = [ + { ...traceGenerator.trace({ numberOfSpans: 3 }), traceID: 1 }, + { ...traceGenerator.trace({ numberOfSpans: 100 }), traceID: idMaxSpans }, + { ...traceGenerator.trace({ numberOfSpans: 5 }), traceID: 3 }, + { ...traceGenerator.trace({ numberOfSpans: 1 }), traceID: idMinSpans }, + ]; + const { traces } = getTraceSummaries(rawTraces); + + const { MOST_SPANS, LEAST_SPANS, LONGEST_FIRST, SHORTEST_FIRST, MOST_RECENT } = orderBy; + + const expecations = { + [MOST_RECENT]: _maxBy(traces, trace => trace.timestamp).traceID, + [LONGEST_FIRST]: _maxBy(traces, trace => trace.duration).traceID, + [SHORTEST_FIRST]: _minBy(traces, trace => trace.duration).traceID, + [MOST_SPANS]: idMaxSpans, + [LEAST_SPANS]: idMinSpans, + }; + expecations.invalidOrderBy = expecations[LONGEST_FIRST]; + + for (const sortBy of Object.keys(expecations)) { + it(`sorts by ${sortBy}`, () => { + const traceID = expecations[sortBy]; + sortTraces(traces, sortBy); + expect(traces[0].traceID).toBe(traceID); + }); + } +}); diff --git a/src/reducers/dependencies.js b/src/reducers/dependencies.js index 885162bb0a..492bc51946 100644 --- a/src/reducers/dependencies.js +++ b/src/reducers/dependencies.js @@ -18,24 +18,33 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -import Immutable from 'immutable'; import { handleActions } from 'redux-actions'; -import * as jaegerApiActions from '../actions/jaeger-api'; +import { fetchDependencies } from '../actions/jaeger-api'; -export const initialState = Immutable.fromJS({ +const initialState = { dependencies: [], loading: false, error: null, -}); +}; + +function fetchStarted(state) { + return { ...state, loading: true }; +} + +function fetchDepsDone(state, { payload }) { + return { ...state, dependencies: payload.data, loading: false }; +} + +function fetchDepsErred(state, { payload: error }) { + return { ...state, error, dependencies: [], loading: false }; +} export default handleActions( { - [`${jaegerApiActions.fetchDependencies}_PENDING`]: state => state.set('loading', true), - [`${jaegerApiActions.fetchDependencies}_FULFILLED`]: (state, { payload: { data: dependencies } }) => - state.set('loading', false).set('dependencies', Immutable.fromJS(dependencies)), - [`${jaegerApiActions.fetchDependencies}_REJECTED`]: (state, { payload: error }) => - state.set('dependencies', Immutable.fromJS([])).set('loading', false).set('error', error), + [`${fetchDependencies}_PENDING`]: fetchStarted, + [`${fetchDependencies}_FULFILLED`]: fetchDepsDone, + [`${fetchDependencies}_REJECTED`]: fetchDepsErred, }, initialState ); diff --git a/src/reducers/dependencies.test.js b/src/reducers/dependencies.test.js new file mode 100644 index 0000000000..d89186de40 --- /dev/null +++ b/src/reducers/dependencies.test.js @@ -0,0 +1,63 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import { fetchDependencies } from '../actions/jaeger-api'; +import reducer from '../../src/reducers/dependencies'; + +const initialState = reducer(undefined, {}); + +function verifyInitialState() { + expect(initialState).toEqual({ + dependencies: [], + loading: false, + error: null, + }); +} + +beforeEach(verifyInitialState); +afterEach(verifyInitialState); + +it('sets loading to true when fetching dependencies is pending', () => { + const state = reducer(initialState, { + type: `${fetchDependencies}_PENDING`, + }); + expect(state.loading).toBe(true); +}); + +it('handles a successful dependencies fetch', () => { + const deps = ['a', 'b', 'c']; + const state = reducer(initialState, { + type: `${fetchDependencies}_FULFILLED`, + payload: { data: deps.slice() }, + }); + expect(state.loading).toBe(false); + expect(state.dependencies).toEqual(deps); +}); + +it('handles a failed dependencies fetch', () => { + const error = new Error('some-message'); + const state = reducer(initialState, { + type: `${fetchDependencies}_REJECTED`, + payload: error, + }); + expect(state.loading).toBe(false); + expect(state.dependencies).toEqual([]); + expect(state.error).toBe(error); +}); diff --git a/src/reducers/services.js b/src/reducers/services.js index 8e9ff45cb1..fabd1fd130 100644 --- a/src/reducers/services.js +++ b/src/reducers/services.js @@ -18,31 +18,51 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -import Immutable from 'immutable'; import { handleActions } from 'redux-actions'; -import * as jaegerApiActions from '../actions/jaeger-api'; +import { fetchServices, fetchServiceOperations as fetchOps } from '../actions/jaeger-api'; -export const initialState = Immutable.fromJS({ +const initialState = { services: [], operationsForService: {}, loading: false, error: null, -}); +}; + +function fetchStarted(state) { + return { ...state, loading: true }; +} + +function fetchServicesDone(state, { payload }) { + const services = payload.data; + return { ...state, services, error: null, loading: false }; +} + +function fetchServicesErred(state, { payload: error }) { + return { ...state, error: error.message, loading: false, services: [] }; +} + +function fetchOpsStarted(state, { meta: { serviceName } }) { + const operationsForService = { ...state.operationsForService, [serviceName]: [] }; + return { ...state, operationsForService }; +} + +function fetchOpsDone(state, { meta, payload }) { + const { data: operations } = payload; + const operationsForService = { ...state.operationsForService, [meta.serviceName]: operations }; + return { ...state, operationsForService }; +} + +// TODO(joe): fetchOpsErred export default handleActions( { - [`${jaegerApiActions.fetchServices}_PENDING`]: state => state.set('loading', true), - [`${jaegerApiActions.fetchServices}_FULFILLED`]: (state, { payload: { data: services } }) => - state.set('loading', false).set('error', null).set('services', Immutable.fromJS(services).sort()), - [`${jaegerApiActions.fetchServices}_REJECTED`]: (state, { payload: error }) => - state.set('services', Immutable.fromJS([])).set('loading', false).set('error', error.message), - [`${jaegerApiActions.fetchServiceOperations}_PENDING`]: (state, { meta: { serviceName } }) => - state.setIn(['operationsForService', serviceName], Immutable.List()), - [`${jaegerApiActions.fetchServiceOperations}_FULFILLED`]: ( - state, - { meta: { serviceName }, payload: { data: operations } } - ) => state.setIn(['operationsForService', serviceName], Immutable.List(operations)), + [`${fetchServices}_PENDING`]: fetchStarted, + [`${fetchServices}_FULFILLED`]: fetchServicesDone, + [`${fetchServices}_REJECTED`]: fetchServicesErred, + + [`${fetchOps}_PENDING`]: fetchOpsStarted, + [`${fetchOps}_FULFILLED`]: fetchOpsDone, }, initialState ); diff --git a/src/reducers/services.test.js b/src/reducers/services.test.js index 557d96dd9a..0a2529c595 100644 --- a/src/reducers/services.test.js +++ b/src/reducers/services.test.js @@ -18,91 +18,76 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -import Immutable from 'immutable'; +import { fetchServices, fetchServiceOperations } from '../../src/actions/jaeger-api'; +import serviceReducer from '../../src/reducers/services'; -import * as jaegerApiActions from '../../src/actions/jaeger-api'; -import serviceReducer, { initialState as servicesInitialState } from '../../src/reducers/services'; +const initialState = serviceReducer(undefined, {}); -it('should initialize an empty services array', () => { - expect( - Immutable.is( - serviceReducer(servicesInitialState, {}), - Immutable.fromJS({ - services: [], - loading: false, - error: null, - operationsForService: {}, - }) - ) - ).toBeTruthy(); -}); +function verifyInitialState() { + expect(initialState).toEqual({ + services: [], + loading: false, + error: null, + operationsForService: {}, + }); +} + +beforeEach(verifyInitialState); +afterEach(verifyInitialState); it('should handle a fetch services with loading state', () => { - expect( - Immutable.is( - serviceReducer(servicesInitialState, { - type: `${jaegerApiActions.fetchServices}_PENDING`, - }), - Immutable.fromJS({ - services: [], - operationsForService: {}, - loading: true, - error: null, - }) - ) - ).toBeTruthy(); + const state = serviceReducer(initialState, { + type: `${fetchServices}_PENDING`, + }); + expect(state).toEqual({ + services: [], + operationsForService: {}, + loading: true, + error: null, + }); }); it('should handle successful services fetch', () => { - expect( - Immutable.is( - serviceReducer(servicesInitialState, { - type: `${jaegerApiActions.fetchServices}_FULFILLED`, - payload: { data: ['a', 'b', 'c'] }, - }), - Immutable.fromJS({ - services: ['a', 'b', 'c'], - operationsForService: {}, - loading: false, - error: null, - }) - ) - ).toBeTruthy(); + const services = ['a', 'b', 'c']; + const state = serviceReducer(initialState, { + type: `${fetchServices}_FULFILLED`, + payload: { data: services.slice() }, + }); + expect(state).toEqual({ + services, + operationsForService: {}, + loading: false, + error: null, + }); }); it('should handle a failed services fetch', () => { - expect( - Immutable.is( - serviceReducer(servicesInitialState, { - type: `${jaegerApiActions.fetchServices}_REJECTED`, - payload: new Error('Error'), - }), - Immutable.fromJS({ - services: [], - operationsForService: {}, - loading: false, - error: 'Error', - }) - ) - ).toBeTruthy(); + const error = new Error('some-message'); + const state = serviceReducer(initialState, { + type: `${fetchServices}_REJECTED`, + payload: error, + }); + expect(state).toEqual({ + services: [], + operationsForService: {}, + loading: false, + error: error.message, + }); }); it('should handle a successful fetching operations for a service ', () => { - expect( - Immutable.is( - serviceReducer(servicesInitialState, { - type: `${jaegerApiActions.fetchServiceOperations}_FULFILLED`, - meta: { serviceName: 'serviceA' }, - payload: { data: ['a', 'b'] }, - }), - Immutable.fromJS({ - services: [], - operationsForService: { - serviceA: ['a', 'b'], - }, - loading: false, - error: null, - }) - ) - ).toBeTruthy(); + const ops = ['a', 'b']; + const state = serviceReducer(initialState, { + type: `${fetchServiceOperations}_FULFILLED`, + meta: { serviceName: 'serviceA' }, + payload: { data: ops.slice() }, + }); + expect(state).toEqual({ + services: [], + operationsForService: { + serviceA: ops, + }, + loading: false, + error: null, + }); }); diff --git a/src/reducers/trace.js b/src/reducers/trace.js index e84360415c..d1aa788825 100644 --- a/src/reducers/trace.js +++ b/src/reducers/trace.js @@ -18,35 +18,52 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -import Immutable from 'immutable'; +import keyBy from 'lodash/keyBy'; import { handleActions } from 'redux-actions'; -import * as jaegerApiActions from '../actions/jaeger-api'; +import { fetchTrace, searchTraces } from '../actions/jaeger-api'; import { enforceUniqueSpanIds } from '../selectors/trace'; -export const initialState = Immutable.Map({ - traces: Immutable.Map(), +const initialState = { + traces: {}, loading: false, error: null, -}); +}; + +function fetchStarted(state) { + return { ...state, loading: true }; +} + +function fetchTraceDone(state, { meta, payload }) { + const trace = enforceUniqueSpanIds(payload.data[0]); + const traces = { ...state.traces, [meta.id]: trace }; + return { ...state, traces, loading: false }; +} + +function fetchTraceErred(state, { meta, payload }) { + const traces = { ...state.traces, [meta.id]: payload }; + return { ...state, traces, loading: false }; +} + +function searchDone(state, { payload }) { + const traces = keyBy(payload.data, 'traceID'); + return { ...state, traces, error: null, loading: false }; +} + +function searchErred(state, action) { + const error = action.payload.message; + return { ...state, error, loading: false, traces: [] }; +} export default handleActions( { - [`${jaegerApiActions.fetchTrace}_PENDING`]: state => state.set('loading', true), - [`${jaegerApiActions.fetchTrace}_FULFILLED`]: (state, { meta: { id }, payload: { data: traces } }) => - state.set('loading', false).setIn(['traces', id], Immutable.fromJS(enforceUniqueSpanIds(traces[0]))), - [`${jaegerApiActions.fetchTrace}_REJECTED`]: (state, { meta: { id }, payload: error }) => - state.set('loading', false).setIn(['traces', id], error), - [`${jaegerApiActions.searchTraces}_PENDING`]: state => state.set('loading', true), - [`${jaegerApiActions.searchTraces}_FULFILLED`]: (state, action) => { - const traceResults = {}; - action.payload.data.forEach(trace => { - traceResults[trace.traceID] = trace; - }); - return state.set('traces', Immutable.fromJS(traceResults)).set('loading', false).set('error', null); - }, - [`${jaegerApiActions.searchTraces}_REJECTED`]: (state, action) => - state.set('traces', Immutable.fromJS([])).set('loading', false).set('error', action.payload.message), + [`${fetchTrace}_PENDING`]: fetchStarted, + [`${fetchTrace}_FULFILLED`]: fetchTraceDone, + [`${fetchTrace}_REJECTED`]: fetchTraceErred, + + [`${searchTraces}_PENDING`]: fetchStarted, + [`${searchTraces}_FULFILLED`]: searchDone, + [`${searchTraces}_REJECTED`]: searchErred, }, initialState ); diff --git a/src/reducers/trace.test.js b/src/reducers/trace.test.js index 2f56cae0aa..b2b293556b 100644 --- a/src/reducers/trace.test.js +++ b/src/reducers/trace.test.js @@ -18,37 +18,28 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -import Immutable from 'immutable'; - import * as jaegerApiActions from '../../src/actions/jaeger-api'; - import traceReducer from '../../src/reducers/trace'; import traceGenerator from '../../src/demo/trace-generators'; -import { getTraceId } from '../../src/selectors/trace'; const generatedTrace = traceGenerator.trace({ numberOfSpans: 1 }); +const { traceID } = generatedTrace; it('trace reducer should set loading true on a fetch', () => { const state = traceReducer(undefined, { type: `${jaegerApiActions.fetchTrace}_PENDING`, - meta: { id: 'whatever' }, }); - - expect(state.get('loading')).toBe(true); + expect(state.loading).toBe(true); }); it('trace reducer should handle a successful FETCH_TRACE', () => { const state = traceReducer(undefined, { type: `${jaegerApiActions.fetchTrace}_FULFILLED`, payload: { data: [generatedTrace] }, - meta: { id: getTraceId(generatedTrace) }, + meta: { id: traceID }, }); - - expect( - Immutable.is(state.get('traces'), Immutable.fromJS({ [getTraceId(generatedTrace)]: generatedTrace })) - ).toBeTruthy(); - - expect(state.get('loading')).toBe(false); + expect(state.traces).toEqual({ [traceID]: generatedTrace }); + expect(state.loading).toBe(false); }); it('trace reducer should handle a failed FETCH_TRACE', () => { @@ -56,14 +47,11 @@ it('trace reducer should handle a failed FETCH_TRACE', () => { const state = traceReducer(undefined, { type: `${jaegerApiActions.fetchTrace}_REJECTED`, payload: error, - meta: { id: generatedTrace.traceID }, + meta: { id: traceID }, }); - - expect( - Immutable.is(state.get('traces'), Immutable.fromJS({ [generatedTrace.traceID]: error })) - ).toBeTruthy(); - - expect(state.get('loading')).toBe(false); + expect(state.traces).toEqual({ [traceID]: error }); + expect(state.traces[traceID]).toBe(error); + expect(state.loading).toBe(false); }); it('trace reducer should handle a successful SEARCH_TRACES', () => { @@ -72,11 +60,6 @@ it('trace reducer should handle a successful SEARCH_TRACES', () => { payload: { data: [generatedTrace] }, meta: { query: 'whatever' }, }); - - const expectedTraces = Immutable.fromJS({ - [generatedTrace.traceID]: generatedTrace, - }); - expect(Immutable.is(state.get('traces'), expectedTraces)).toBeTruthy(); - - expect(state.get('loading')).toBe(false); + expect(state.traces).toEqual({ [traceID]: generatedTrace }); + expect(state.loading).toBe(false); }); diff --git a/src/selectors/dependencies.js b/src/selectors/dependencies.js index 04393cd617..4814c75dec 100644 --- a/src/selectors/dependencies.js +++ b/src/selectors/dependencies.js @@ -21,7 +21,7 @@ import { createSelector } from 'reselect'; export const formatDependenciesAsNodesAndLinks = createSelector( - ({ dependencies }) => dependencies.toJS(), + ({ dependencies }) => dependencies, dependencies => { const data = dependencies.reduce( (response, link) => { diff --git a/src/selectors/search.js b/src/selectors/search.js deleted file mode 100644 index 0199b7da65..0000000000 --- a/src/selectors/search.js +++ /dev/null @@ -1,141 +0,0 @@ -// Copyright (c) 2017 Uber Technologies, Inc. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import { createSelector } from 'reselect'; -import { sortBy } from 'lodash'; -import { getTraceTimestamp, getTraceDuration, getTraceName } from './trace'; -import { getPercentageOfDuration } from '../utils/date'; - -const getTraces = state => state.traces; -const getTrace = state => state.trace; - -/** - * Helper function to help use calculate what percentage of the total duration - * a service has been in the trace. - */ -export function calculatePercentOfTotal(timestamps) { - const timestampsByStartTime = sortBy(timestamps, t => t[0]); - let lastTimestamp; - const duration = timestampsByStartTime.reduce((lastDuration, t) => { - let newDuration; - if (lastTimestamp >= t[1]) { - newDuration = lastDuration; - } else if (lastTimestamp > t[0] && lastTimestamp < t[1]) { - newDuration = lastDuration + (t[1] - lastTimestamp); - lastTimestamp = t[1]; - } else { - newDuration = lastDuration + (t[1] - t[0]); - lastTimestamp = t[1]; - } - return newDuration; - }, 0); - return duration; -} - -export function transformTrace(trace) { - const processes = trace.processes || {}; - const processMap = {}; - if (trace.spans && trace.spans.length) { - trace.spans.forEach(span => { - if (!span.startTime || !span.duration) { - return; - } - const processName = processes[span.processID].serviceName; - if (!processMap[processName]) { - processMap[processName] = []; - } - processMap[processName].push([span.startTime, span.startTime + span.duration]); - }); - } - - const traceDuration = getTraceDuration(trace); - const services = Object.keys(processMap).map(processName => { - const timestamps = processMap[processName]; - return { - name: processName, - numberOfApperancesInTrace: timestamps.length, - percentOfTrace: Math.round( - getPercentageOfDuration(calculatePercentOfTotal(timestamps), traceDuration), - -1 - ), - }; - }); - - const isErredTag = ({ key, value }) => key === 'error' && value === true; - const numberOfErredSpans = trace.spans.reduce( - (total, { tags }) => total + Number(tags.some(isErredTag)), - 0 - ); - - return { - traceName: getTraceName(trace), - traceID: trace.traceID, - numberOfSpans: trace.spans.length, - duration: traceDuration / 1000, - timestamp: Math.floor(getTraceTimestamp(trace) / 1000), - numberOfErredSpans, - services, - }; -} - -export const transformTraceSelector = createSelector(getTrace, transformTrace); - -export function transformTraceResults(rawTraces) { - let maxDuration = 0; - const traces = rawTraces.map(trace => { - const transformedTrace = transformTraceSelector({ trace }); - // Caluculate max duration of traces. - if (transformedTrace.duration > maxDuration) { - maxDuration = transformedTrace.duration; - } - return transformedTrace; - }); - return { - traces, - maxDuration, - }; -} -export const transformTraceResultsSelector = createSelector(getTraces, traces => - transformTraceResults(traces) -); - -// Sorting options -export const MOST_RECENT = 'MOST_RECENT'; -export const LONGEST_FIRST = 'LONGEST_FIRST'; -export const SHORTEST_FIRST = 'SHORTEST_FIRST'; -export const MOST_SPANS = 'MOST_SPANS'; -export const LEAST_SPANS = 'LEAST_SPANS'; -export function getSortedTraceResults(traces, sortByFilter) { - return traces.sort((t1, t2) => { - switch (sortByFilter) { - case MOST_RECENT: - return t2.timestamp - t1.timestamp; - case SHORTEST_FIRST: - return t1.duration - t2.duration; - case MOST_SPANS: - return t2.numberOfSpans - t1.numberOfSpans; - case LEAST_SPANS: - return t1.numberOfSpans - t2.numberOfSpans; - case LONGEST_FIRST: - default: - return t2.duration - t1.duration; - } - }); -} diff --git a/src/selectors/search.test.js b/src/selectors/search.test.js deleted file mode 100644 index 6cfe10b0db..0000000000 --- a/src/selectors/search.test.js +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright (c) 2017 Uber Technologies, Inc. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import { maxBy, minBy } from 'lodash'; - -import * as searchSelectors from './search'; -import traceGenerator from '../demo/trace-generators'; - -it('transformTrace() works accurately', () => { - const trace = traceGenerator.trace({}); - const transformedTrace = searchSelectors.transformTrace(trace); - - expect(transformedTrace.numberOfSpans).toBe(trace.spans.length); - - expect(transformedTrace.duration).toBe(trace.duration / 1000); - - expect(transformedTrace.timestamp).toBe(Math.floor(trace.timestamp / 1000)); - - const erredTag = { key: 'error', type: 'bool', value: true }; - - expect(transformedTrace.numberOfErredSpans).toBe(0); - - trace.spans[0].tags.push(erredTag); - expect(searchSelectors.transformTrace(trace).numberOfErredSpans).toBe(1); - - trace.spans[1].tags.push(erredTag); - expect(searchSelectors.transformTrace(trace).numberOfErredSpans).toBe(2); -}); - -it('transformTraceResults() calculates the max duration of all traces', () => { - const traces = [traceGenerator.trace({}), traceGenerator.trace({})]; - const traceDurationOne = searchSelectors.transformTrace(traces[0]).duration; - const traceDurationTwo = searchSelectors.transformTrace(traces[1]).duration; - - const expectedMaxDuration = traceDurationOne > traceDurationTwo ? traceDurationOne : traceDurationTwo; - - const { maxDuration } = searchSelectors.transformTraceResults(traces); - - expect(maxDuration).toBe(expectedMaxDuration); -}); - -it('getSortedTraceResults() sorting works', () => { - const testTraces = [ - { ...traceGenerator.trace({ numberOfSpans: 3 }), traceID: 1 }, - { ...traceGenerator.trace({ numberOfSpans: 100 }), traceID: 2 }, - { ...traceGenerator.trace({ numberOfSpans: 5 }), traceID: 3 }, - { ...traceGenerator.trace({ numberOfSpans: 1 }), traceID: 4 }, - ]; - const { - getSortedTraceResults, - MOST_SPANS, - LEAST_SPANS, - LONGEST_FIRST, - SHORTEST_FIRST, - MOST_RECENT, - } = searchSelectors; - - const { traces } = searchSelectors.transformTraceResults(testTraces); - const maxDurationTraceID = maxBy(traces, trace => trace.duration).traceID; - const minDurationTraceID = minBy(traces, trace => trace.duration).traceID; - const mostRecentTraceID = maxBy(traces, trace => trace.timestamp).traceID; - expect(getSortedTraceResults(traces, MOST_RECENT)[0].traceID).toBe(mostRecentTraceID); - - expect(getSortedTraceResults(traces, LONGEST_FIRST)[0].traceID).toBe(maxDurationTraceID); - - expect(getSortedTraceResults(traces, SHORTEST_FIRST)[0].traceID).toBe(minDurationTraceID); - - expect(getSortedTraceResults(traces, MOST_SPANS)[0].traceID).toBe(2); - - expect(getSortedTraceResults(traces, LEAST_SPANS)[0].traceID).toBe(4); - expect(getSortedTraceResults(traces, 'invalid')[0].traceID).toBe(maxDurationTraceID); -}); - -it('calculatePercentOfTotal() works properly', () => { - const testCases = [ - { - input: [[0, 3], [1, 3], [1, 4], [9, 10]], - expectedOutput: 5, - }, - { - input: [[1, 3], [1, 4], [9, 10], [0, 11]], - expectedOutput: 11, - }, - { - input: [[0, 10], [15, 20]], - expectedOutput: 15, - }, - ]; - testCases.forEach(testCase => { - expect(searchSelectors.calculatePercentOfTotal(testCase.input)).toBe(testCase.expectedOutput); - }); -}); diff --git a/src/types/index.js b/src/types/index.js index 3a2bf61759..c37faaa7ff 100644 --- a/src/types/index.js +++ b/src/types/index.js @@ -64,6 +64,6 @@ export type Trace = { traceID: string, spans: Array, processes: { - [processID: string]: Process, + [string]: Process, }, }; diff --git a/src/types/search.js b/src/types/search.js new file mode 100644 index 0000000000..1ac54f0116 --- /dev/null +++ b/src/types/search.js @@ -0,0 +1,48 @@ +// @flow + +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +export type TraceSummary = { + /** + * Duration of trace in milliseconds. + * @type {number} + */ + duration: number, + /** + * Start time of trace in milliseconds. + * @type {number} + */ + timestamp: number, + traceName: string, + traceID: string, + numberOfErredSpans: number, + numberOfSpans: number, + services: { name: string, numberOfSpans: number }[], +}; + +export type TraceSummaries = { + /** + * Duration of longest trace in `traces` in milliseconds. + * @type {[type]} + */ + maxDuration: number, + traces: TraceSummary[], +}; diff --git a/yarn.lock b/yarn.lock index 48e8326b96..d73ef5184d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3009,10 +3009,6 @@ ignore@^3.2.0: version "3.2.6" resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.2.6.tgz#26e8da0644be0bb4cb39516f6c79f0e0f4ffe48c" -immutable@^3.8.1: - version "3.8.1" - resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.1.tgz#200807f11ab0f72710ea485542de088075f68cd2" - imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" @@ -5085,10 +5081,6 @@ react-helmet@^3.1.0: object-assign "^4.0.1" react-side-effect "^1.1.0" -react-immutable-proptypes@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/react-immutable-proptypes/-/react-immutable-proptypes-2.1.0.tgz#023d6f39bb15c97c071e9e60d00d136eac5fa0b4" - react-metrics@^2.2.3: version "2.3.0" resolved "https://registry.yarnpkg.com/react-metrics/-/react-metrics-2.3.0.tgz#61e5531875da43b90295022e66e572e2b1a25a2a"