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