Skip to content

Commit

Permalink
Trace quality view & Ddg Decorations (#564)
Browse files Browse the repository at this point in the history
* WIP: Action and types for decorations

Signed-off-by: Everett Ross <reverett@uber.com>

* Add PAD reducer, fix types, fix year

Signed-off-by: Everett Ross <reverett@uber.com>

* Fix and test reducer, fix types, fix another year

Signed-off-by: Everett Ross <reverett@uber.com>

* Add another pad reducer test

Signed-off-by: Everett Ross <reverett@uber.com>

* WIP: Begin testing action

Signed-off-by: Everett Ross <reverett@uber.com>

* WIP: Finish action tests TODO: Move stringSupplant

Signed-off-by: Everett Ross <reverett@uber.com>

* Move and test stringSupplant

Signed-off-by: Everett Ross <reverett@uber.com>

* Cleanup

Signed-off-by: Everett Ross <reverett@uber.com>

* WIP: Decorate nodes, selector/detail side panel

Signed-off-by: Everett Ross <reverett@uber.com>

* WIP: Style side panel

Signed-off-by: Everett Ross <reverett@uber.com>

* WIP: Continue styling side panel, fetch details in
details panel, render details in details card
TODO: Style table, handle list, handle styled values

Signed-off-by: Everett Ross <reverett@uber.com>

* WIP: Improve TS handling of union of arrays
TODO: Style table, handle list, handle styled values

Signed-off-by: Everett Ross <reverett@uber.com>

* WIP: Limit % circle size, update cursor for
clickable ddg nodes, fix resizer height css, make top offset css var
TODO: Style table, handle list, handle styled values

Signed-off-by: Everett Ross <reverett@uber.com>

* WIP: Handle list, begin overflow management
TODO: Style table overflow, handle styled values

Signed-off-by: Everett Ross <reverett@uber.com>

* WIP: Manage overflow, begin handling styled values
TODO: Handle styled values, loading&err render, modal, beautification

Signed-off-by: Everett Ross <reverett@uber.com>

* WIP: Handle styled values, render loading&err,
style card
TODO: Add info modal

Signed-off-by: Everett Ross <reverett@uber.com>

* Add info modal, begin clean up TODO clean up&test

Signed-off-by: Everett Ross <reverett@uber.com>

* Fix: rowKeys, setViewModifier argument name,
decoration header size, destructured variable order,
DeepDependencies/index initial state, stale comments, yarn.lock
@types/node, yarn.lock registry urls, 'value' in paths, existing tests,
op-specific details
Add: linking row cells
TODO: New tests

Signed-off-by: Everett Ross <reverett@uber.com>

* Handle linked cells, fix cell sort order

Signed-off-by: Everett Ross <reverett@uber.com>

* Test existing files, track decorations viewed,
memoize summary requests
TODO: Test SidePanel/ index, index.track, DetailsPanel

Signed-off-by: Everett Ross <reverett@uber.com>

* Test SidePanel/ index&track WIP test DetailsPanel

Signed-off-by: Everett Ross <reverett@uber.com>

* WIP test DetailsPanel

Signed-off-by: Everett Ross <reverett@uber.com>

* Finish DetailsPanel tests

Signed-off-by: Everett Ross <reverett@uber.com>

* Clean up

Signed-off-by: Everett Ross <reverett@uber.com>

* Add skeleton components and fetch quality metrics

Signed-off-by: Everett Ross <reverett@uber.com>

* WIP: Render all data and dropdowns except banner
TODO: Render banner text, style components, test

Signed-off-by: Everett Ross <reverett@uber.com>

* WIP: Style components, implement lookback
TODO: Render banner text, render weight, test

Signed-off-by: Everett Ross <reverett@uber.com>

* Debounce InputNumber, limit search url length, add
metric documentation tooltip, tweak styles, add BannerText, handle
loading, handle error
TODO: test, cleanup

Signed-off-by: Everett Ross <reverett@uber.com>

* Cleanup

Signed-off-by: Everett Ross <reverett@uber.com>

* Add support for decoration links

Signed-off-by: Everett Ross <reverett@uber.com>

* Clean up and add quality-metrics top nav link

Signed-off-by: Everett Ross <reverett@uber.com>
  • Loading branch information
everett980 authored Apr 27, 2020
1 parent 2a0321e commit 3e17a7c
Show file tree
Hide file tree
Showing 58 changed files with 4,161 additions and 236 deletions.
1 change: 1 addition & 0 deletions packages/jaeger-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
"query-string": "^6.3.0",
"raven-js": "^3.22.1",
"react": "^16.3.2",
"react-circular-progressbar": "^2.0.3",
"react-dimensions": "^1.3.0",
"react-dom": "^16.3.2",
"react-ga": "^2.4.1",
Expand Down
82 changes: 42 additions & 40 deletions packages/jaeger-ui/src/actions/path-agnostic-decorations.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,23 @@

import _set from 'lodash/set';

import { processed, getDecoration as getDecorationImpl } from './path-agnostic-decorations';
import { _processed, getDecoration as getDecorationImpl } from './path-agnostic-decorations';
import * as getConfig from '../utils/config/get-config';
import stringSupplant from '../utils/stringSupplant';
import JaegerAPI from '../api/jaeger';

jest.mock('lru-memoize', () => () => x => x);

describe('getDecoration', () => {
let getConfigValueSpy;
let fetchDecorationSpy;
let resolves;
let rejects;

const opUrl = 'opUrl?service=#service&operation=#operation';
const url = 'opUrl?service=#service';
const valuePath = 'withoutOpPath.#service';
const opValuePath = 'opPath.#service.#operation';
const opSummaryUrl = 'opSummaryUrl?service=#service&operation=#operation';
const summaryUrl = 'summaryUrl?service=#service';
const summaryPath = 'withoutOpPath.#service';
const opSummaryPath = 'opPath.#service.#operation';
const withOpID = 'decoration id with op url and op path';
const partialID = 'decoration id with op url without op path';
const withoutOpID = 'decoration id with only url';
Expand All @@ -48,21 +50,21 @@ describe('getDecoration', () => {
getConfigValueSpy = jest.spyOn(getConfig, 'getConfigValue').mockReturnValue([
{
id: withOpID,
url,
opUrl,
valuePath,
opValuePath,
summaryUrl,
opSummaryUrl,
summaryPath,
opSummaryPath,
},
{
id: partialID,
url,
opUrl,
valuePath,
summaryUrl,
opSummaryUrl,
summaryPath,
},
{
id: withoutOpID,
url,
valuePath,
summaryUrl,
summaryPath,
},
]);
fetchDecorationSpy = jest.spyOn(JaegerAPI, 'fetchDecoration').mockImplementation(
Expand All @@ -76,7 +78,7 @@ describe('getDecoration', () => {

beforeEach(() => {
fetchDecorationSpy.mockClear();
processed.clear();
_processed.clear();
resolves = [];
rejects = [];
});
Expand All @@ -102,42 +104,42 @@ describe('getDecoration', () => {

it('resolves to include single response for op decoration given op', async () => {
const promise = getDecoration(withOpID, service, operation);
resolves[0](_set({}, stringSupplant(opValuePath, { service, operation }), testVal));
resolves[0](_set({}, stringSupplant(opSummaryPath, { service, operation }), testVal));
const res = await promise;
expect(res).toEqual(_set({}, `${withOpID}.withOp.${service}.${operation}`, testVal));
expect(fetchDecorationSpy).toHaveBeenLastCalledWith(stringSupplant(opUrl, { service, operation }));
expect(fetchDecorationSpy).toHaveBeenLastCalledWith(stringSupplant(opSummaryUrl, { service, operation }));
});

it('resolves to include single response for op decoration not given op', async () => {
const promise = getDecoration(withOpID, service);
resolves[0](_set({}, stringSupplant(valuePath, { service }), testVal));
resolves[0](_set({}, stringSupplant(summaryPath, { service }), testVal));
const res = await promise;
expect(res).toEqual(_set({}, `${withOpID}.withoutOp.${service}`, testVal));
expect(fetchDecorationSpy).toHaveBeenLastCalledWith(stringSupplant(url, { service }));
expect(fetchDecorationSpy).toHaveBeenLastCalledWith(stringSupplant(summaryUrl, { service }));
});

it('resolves to include single response for malformed op decoration given op', async () => {
const promise = getDecoration(partialID, service, operation);
resolves[0](_set({}, stringSupplant(valuePath, { service }), testVal));
resolves[0](_set({}, stringSupplant(summaryPath, { service }), testVal));
const res = await promise;
expect(res).toEqual(_set({}, `${partialID}.withoutOp.${service}`, testVal));
expect(fetchDecorationSpy).toHaveBeenLastCalledWith(stringSupplant(url, { service }));
expect(fetchDecorationSpy).toHaveBeenLastCalledWith(stringSupplant(summaryUrl, { service }));
});

it('resolves to include single response for svc decoration given op', async () => {
const promise = getDecoration(withoutOpID, service, operation);
resolves[0](_set({}, stringSupplant(valuePath, { service }), testVal));
resolves[0](_set({}, stringSupplant(summaryPath, { service }), testVal));
const res = await promise;
expect(res).toEqual(_set({}, `${withoutOpID}.withoutOp.${service}`, testVal));
expect(fetchDecorationSpy).toHaveBeenLastCalledWith(stringSupplant(url, { service }));
expect(fetchDecorationSpy).toHaveBeenLastCalledWith(stringSupplant(summaryUrl, { service }));
});

it('resolves to include single response for svc decoration not given op', async () => {
const promise = getDecoration(withoutOpID, service);
resolves[0](_set({}, stringSupplant(valuePath, { service }), testVal));
resolves[0](_set({}, stringSupplant(summaryPath, { service }), testVal));
const res = await promise;
expect(res).toEqual(_set({}, `${withoutOpID}.withoutOp.${service}`, testVal));
expect(fetchDecorationSpy).toHaveBeenLastCalledWith(stringSupplant(url, { service }));
expect(fetchDecorationSpy).toHaveBeenLastCalledWith(stringSupplant(summaryUrl, { service }));
});

it('handles error responses', async () => {
Expand All @@ -148,7 +150,7 @@ describe('getDecoration', () => {
expect(res0).toEqual(
_set({}, `${withoutOpID}.withoutOp.${service}`, `Unable to fetch decoration: ${message}`)
);
expect(fetchDecorationSpy).toHaveBeenLastCalledWith(stringSupplant(url, { service }));
expect(fetchDecorationSpy).toHaveBeenLastCalledWith(stringSupplant(summaryUrl, { service }));

const err = 'foo error without message';
const promise1 = getDecoration(withOpID, service, operation);
Expand All @@ -157,21 +159,21 @@ describe('getDecoration', () => {
expect(res1).toEqual(
_set({}, `${withOpID}.withOp.${service}.${operation}`, `Unable to fetch decoration: ${err}`)
);
expect(fetchDecorationSpy).toHaveBeenLastCalledWith(stringSupplant(opUrl, { service, operation }));
expect(fetchDecorationSpy).toHaveBeenLastCalledWith(stringSupplant(opSummaryUrl, { service, operation }));
});

it('defaults value if valuePath not found in response', async () => {
it('defaults value if summaryPath not found in response', async () => {
const promise = getDecoration(withoutOpID, service);
resolves[0]();
const res = await promise;
expect(res).toEqual(
_set(
{},
`${withoutOpID}.withoutOp.${service}`,
`${stringSupplant(valuePath, { service })} not found in response`
`\`${stringSupplant(summaryPath, { service })}\` not found in response`
)
);
expect(fetchDecorationSpy).toHaveBeenLastCalledWith(stringSupplant(url, { service }));
expect(fetchDecorationSpy).toHaveBeenLastCalledWith(stringSupplant(summaryUrl, { service }));
});

it('returns undefined if invoked before previous invocation is resolved', () => {
Expand All @@ -182,13 +184,13 @@ describe('getDecoration', () => {
it('resolves to include responses for all concurrent requests', async () => {
const otherOp = 'other op';
const promise = getDecoration(withOpID, service, operation);
resolves[0](_set({}, stringSupplant(opValuePath, { service, operation }), testVal));
resolves[0](_set({}, stringSupplant(opSummaryPath, { service, operation }), testVal));
getDecoration(partialID, service, operation);
resolves[1](_set({}, stringSupplant(valuePath, { service }), testVal));
resolves[1](_set({}, stringSupplant(summaryPath, { service }), testVal));
getDecoration(withOpID, service);
resolves[2](_set({}, stringSupplant(valuePath, { service }), testVal));
resolves[2](_set({}, stringSupplant(summaryPath, { service }), testVal));
getDecoration(withoutOpID, service);
resolves[3](_set({}, stringSupplant(valuePath, { service }), testVal));
resolves[3](_set({}, stringSupplant(summaryPath, { service }), testVal));
const message = 'foo error message';
getDecoration(withOpID, service, otherOp);
rejects[4]({ message });
Expand Down Expand Up @@ -222,15 +224,15 @@ describe('getDecoration', () => {
it('scopes promises to not include previous promise results', async () => {
const otherOp = 'other op';
const promise0 = getDecoration(withOpID, service, operation);
resolves[0](_set({}, stringSupplant(opValuePath, { service, operation }), testVal));
resolves[0](_set({}, stringSupplant(opSummaryPath, { service, operation }), testVal));
getDecoration(partialID, service, operation);
resolves[1](_set({}, stringSupplant(valuePath, { service }), testVal));
resolves[1](_set({}, stringSupplant(summaryPath, { service }), testVal));
const res0 = await promise0;

const promise1 = getDecoration(withOpID, service);
resolves[2](_set({}, stringSupplant(valuePath, { service }), testVal));
resolves[2](_set({}, stringSupplant(summaryPath, { service }), testVal));
getDecoration(withoutOpID, service);
resolves[3](_set({}, stringSupplant(valuePath, { service }), testVal));
resolves[3](_set({}, stringSupplant(summaryPath, { service }), testVal));
const message = 'foo error message';
getDecoration(withOpID, service, otherOp);
rejects[4]({ message });
Expand Down Expand Up @@ -272,15 +274,15 @@ describe('getDecoration', () => {

it('no-ops for already processed id, service, and operation', async () => {
const promise0 = getDecoration(withOpID, service, operation);
resolves[0](_set({}, stringSupplant(opValuePath, { service, operation }), testVal));
resolves[0](_set({}, stringSupplant(opSummaryPath, { service, operation }), testVal));
const res0 = await promise0;
expect(res0).toEqual(_set({}, `${withOpID}.withOp.${service}.${operation}`, testVal));

const promise1 = getDecoration(withOpID, service, operation);
expect(promise1).toBeUndefined();

const promise2 = getDecoration(withoutOpID, service);
resolves[1](_set({}, stringSupplant(valuePath, { service }), testVal));
resolves[1](_set({}, stringSupplant(summaryPath, { service }), testVal));
const res1 = await promise2;
expect(res1).toEqual(_set({}, `${withoutOpID}.withoutOp.${service}`, testVal));
expect(fetchDecorationSpy).toHaveBeenCalledTimes(2);
Expand Down
28 changes: 16 additions & 12 deletions packages/jaeger-ui/src/actions/path-agnostic-decorations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import _get from 'lodash/get';
import _memoize from 'lodash/memoize';
import _set from 'lodash/set';
import memoize from 'lru-memoize';
import { createActions, ActionFunctionAny, Action } from 'redux-actions';

import JaegerAPI from '../api/jaeger';
Expand All @@ -23,9 +24,12 @@ import { getConfigValue } from '../utils/config/get-config';
import generateActionTypes from '../utils/generate-action-types';
import stringSupplant from '../utils/stringSupplant';

// wrapping JaegerAPI.fetchDecoration is necessary for tests to properly mock inside memoization
const fetchDecoration = memoize(10)((url: string) => JaegerAPI.fetchDecoration(url));

export const actionTypes = generateActionTypes('@jaeger-ui/PATH_AGNOSTIC_DECORATIONS', ['GET_DECORATION']);

const getDecorationSchema = _memoize((id: string): TPathAgnosticDecorationSchema | undefined => {
export const getDecorationSchema = _memoize((id: string): TPathAgnosticDecorationSchema | undefined => {
const schemas = getConfigValue('pathAgnosticDecorations') as TPathAgnosticDecorationSchema[] | undefined;
if (!schemas) return undefined;
return schemas.find(s => s.id === id);
Expand All @@ -39,16 +43,17 @@ let resolve: undefined | ((arg: TNewData) => void);

// Bespoke memoization-adjacent solution necessary as this should return `undefined`, not an old promise, on
// duplicate calls
export const processed = new Map<string, Map<string, Set<string | undefined>>>();
// exported for tests
export const _processed = new Map<string, Map<string, Set<string | undefined>>>();

export function getDecoration(
id: string,
service: string,
operation?: string
): Promise<TNewData> | undefined {
const processedID = processed.get(id);
const processedID = _processed.get(id);
if (!processedID) {
processed.set(id, new Map<string, Set<string | undefined>>([[service, new Set([operation])]]));
_processed.set(id, new Map<string, Set<string | undefined>>([[service, new Set([operation])]]));
} else {
const processedService = processedID.get(service);
if (!processedService) processedID.set(service, new Set([operation]));
Expand All @@ -67,24 +72,23 @@ export function getDecoration(
}

pendingCount = pendingCount ? pendingCount + 1 : 1;
const { url, opUrl, valuePath, opValuePath } = schema;
const { summaryUrl, opSummaryUrl, summaryPath, opSummaryPath } = schema;
let promise: Promise<Record<string, any>>;
let getPath: string;
let setPath: string;
if (opValuePath && opUrl && operation) {
promise = JaegerAPI.fetchDecoration(stringSupplant(opUrl, { service, operation }));
getPath = stringSupplant(opValuePath, { service, operation });
if (opSummaryPath && opSummaryUrl && operation) {
promise = fetchDecoration(stringSupplant(opSummaryUrl, { service, operation }));
getPath = stringSupplant(opSummaryPath, { service, operation });
setPath = `${id}.withOp.${service}.${operation}`;
} else {
promise = JaegerAPI.fetchDecoration(stringSupplant(url, { service }));
getPath = stringSupplant(valuePath, { service });
getPath = valuePath;
promise = fetchDecoration(stringSupplant(summaryUrl, { service }));
getPath = stringSupplant(summaryPath, { service });
setPath = `${id}.withoutOp.${service}`;
}

promise
.then(res => {
return _get(res, getPath, `${getPath} not found in response`);
return _get(res, getPath, `\`${getPath}\` not found in response`);
})
.catch(err => {
return `Unable to fetch decoration: ${err.message || err}`;
Expand Down
3 changes: 3 additions & 0 deletions packages/jaeger-ui/src/api/jaeger.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ const JaegerAPI = {
archiveTrace(id) {
return getJSON(`${this.apiRoot}archive/${id}`, { method: 'POST' });
},
fetchQualityMetrics(service, lookback) {
return getJSON(`/qualitymetrics-v2`, { query: { service, lookback } });
},
fetchDecoration(url) {
return getJSON(url);
},
Expand Down
4 changes: 2 additions & 2 deletions packages/jaeger-ui/src/components/App/Page.css
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ limitations under the License.
display: flex;
flex-direction: column;
left: 0;
min-height: calc(100% - 46px);
min-height: calc(100% - var(--nav-height));
position: absolute;
right: 0;
top: 46px;
top: var(--nav-height);
}
9 changes: 9 additions & 0 deletions packages/jaeger-ui/src/components/App/TopNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { RouteComponentProps, Link, withRouter } from 'react-router-dom';
import TraceIDSearchInput from './TraceIDSearchInput';
import * as dependencyGraph from '../DependencyGraph/url';
import * as deepDependencies from '../DeepDependencies/url';
import * as qualityMetrics from '../QualityMetrics/url';
import * as searchUrl from '../SearchTracePage/url';
import * as diffUrl from '../TraceDiff/url';
import { ReduxState } from '../../types';
Expand Down Expand Up @@ -59,6 +60,14 @@ if (getConfigValue('deepDependencies.menuEnabled')) {
});
}

if (getConfigValue('qualityMetrics.menuEnabled')) {
NAV_LINKS.push({
to: qualityMetrics.getUrl(),
matches: qualityMetrics.matches,
text: 'Quality Metrics',
});
}

function getItemLink(item: ConfigMenuItem) {
const { label, anchorTarget, url } = item;
return (
Expand Down
3 changes: 3 additions & 0 deletions packages/jaeger-ui/src/components/App/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import DependencyGraph from '../DependencyGraph';
import { ROUTE_PATH as dependenciesPath } from '../DependencyGraph/url';
import DeepDependencies from '../DeepDependencies';
import { ROUTE_PATH as deepDependenciesPath } from '../DeepDependencies/url';
import QualityMetrics from '../QualityMetrics';
import { ROUTE_PATH as qualityMetricsPath } from '../QualityMetrics/url';
import SearchTracePage from '../SearchTracePage';
import { ROUTE_PATH as searchPath } from '../SearchTracePage/url';
import TraceDiff from '../TraceDiff';
Expand Down Expand Up @@ -60,6 +62,7 @@ export default class JaegerUIApp extends Component {
<Route path={tracePath} component={TracePage} />
<Route path={dependenciesPath} component={DependencyGraph} />
<Route path={deepDependenciesPath} component={DeepDependencies} />
<Route path={qualityMetricsPath} component={QualityMetrics} />

<Redirect exact path="/" to={searchPath} />
<Redirect exact path={prefixUrl()} to={searchPath} />
Expand Down
Loading

0 comments on commit 3e17a7c

Please sign in to comment.