From 0c9ff94d93a7410c7f2fcbc340f51f994ec463fc Mon Sep 17 00:00:00 2001 From: Joe Farro Date: Sat, 10 Feb 2018 08:02:33 -0500 Subject: [PATCH 01/18] Track errors in GA w raven-js; TODO: tests, readme Signed-off-by: Joe Farro --- flow-typed/npm/raven-js_v3.17.x.js | 226 +++++++++++++++++++++++++ package.json | 7 +- scripts/deploy-docs.sh | 8 - scripts/get-tracking-version.js | 92 ++++++++++ src/components/App/Page.js | 2 +- src/components/App/Page.test.js | 4 +- src/index.js | 12 +- src/utils/metrics.js | 29 ---- src/utils/tracking/conv-raven-to-ga.js | 187 ++++++++++++++++++++ src/utils/tracking/index.js | 185 ++++++++++++++++++++ yarn.lock | 19 ++- 11 files changed, 716 insertions(+), 55 deletions(-) create mode 100644 flow-typed/npm/raven-js_v3.17.x.js delete mode 100755 scripts/deploy-docs.sh create mode 100755 scripts/get-tracking-version.js delete mode 100644 src/utils/metrics.js create mode 100644 src/utils/tracking/conv-raven-to-ga.js create mode 100644 src/utils/tracking/index.js diff --git a/flow-typed/npm/raven-js_v3.17.x.js b/flow-typed/npm/raven-js_v3.17.x.js new file mode 100644 index 0000000000..020cd96c7a --- /dev/null +++ b/flow-typed/npm/raven-js_v3.17.x.js @@ -0,0 +1,226 @@ +// flow-typed signature: e1f97cc57b871f5647a2a5a8567b0b5b +// flow-typed version: 39e54508d9/raven-js_v3.17.x/flow_>=v0.38.x + +type LogLevel = 'critical' | 'error' | 'warning' | 'info' | 'debug'; + +type AutoBreadcrumbOptions = { + xhr?: boolean, + console?: boolean, + dom?: boolean, + location?: boolean, +}; + +type RavenInstrumentationOptions = { + tryCatch?: boolean, +}; + +type Breadcrumb = { + message?: string, + category?: string, + level?: LogLevel, + data?: any, + type?: BreadcrumbType, +}; + +type BreadcrumbType = 'navigation' | 'http'; + +type RavenOptions = { + /** The log level associated with this event. Default: error */ + level?: LogLevel, + + /** The name of the logger used by Sentry. Default: javascript */ + logger?: string, + + /** The environment of the application you are monitoring with Sentry */ + environment?: string, + + /** The release version of the application you are monitoring with Sentry */ + release?: string, + + /** The name of the server or device that the client is running on */ + serverName?: string, + + /** List of messages to be filtered out before being sent to Sentry. */ + ignoreErrors?: (RegExp | string)[], + + /** Similar to ignoreErrors, but will ignore errors from whole urls patching a regex pattern. */ + ignoreUrls?: (RegExp | string)[], + + /** The inverse of ignoreUrls. Only report errors from whole urls matching a regex pattern. */ + whitelistUrls?: (RegExp | string)[], + + /** An array of regex patterns to indicate which urls are a part of your app. */ + includePaths?: (RegExp | string)[], + + /** Additional data to be tagged onto the error. */ + tags?: { + [id: string]: string, + }, + + /** set to true to get the stack trace of your message */ + stacktrace?: boolean, + + extra?: any, + + /** In some cases you may see issues where Sentry groups multiple events together when they should be separate entities. In other cases, Sentry simply doesn’t group events together because they’re so sporadic that they never look the same. */ + fingerprint?: string[], + + /** A function which allows mutation of the data payload right before being sent to Sentry */ + dataCallback?: (data: any) => any, + + /** A callback function that allows you to apply your own filters to determine if the message should be sent to Sentry. */ + shouldSendCallback?: (data: any) => boolean, + + /** By default, Raven does not truncate messages. If you need to truncate characters for whatever reason, you may set this to limit the length. */ + maxMessageLength?: number, + + /** By default, Raven will truncate URLs as they appear in breadcrumbs and other meta interfaces to 250 characters in order to minimize bytes over the wire. This does *not* affect URLs in stack traces. */ + maxUrlLength?: number, + + /** Override the default HTTP data transport handler. */ + transport?: (options: RavenTransportOptions) => void, + + /** Allow use of private/secretKey. */ + allowSecretKey?: boolean, + + /** Enables/disables instrumentation of globals. */ + instrument?: boolean | RavenInstrumentationOptions, + + /** Enables/disables automatic collection of breadcrumbs. */ + autoBreadcrumbs?: boolean | AutoBreadcrumbOptions, +}; + +type RavenTransportOptions = { + url: string, + data: any, + auth: { + sentry_version: string, + sentry_client: string, + sentry_key: string, + }, + onSuccess: () => void, + onFailure: () => void, +}; + +declare module 'raven-js' { + declare type RavenPlugin = { + (raven: Raven, ...args: any[]): Raven, + }; + + declare class Raven { + /** Raven.js version. */ + VERSION: string; + + Plugins: { [id: string]: RavenPlugin }; + + /* + * Allow Raven to be configured as soon as it is loaded + * It uses a global RavenConfig = {dsn: '...', config: {}} + */ + afterLoad(): void; + + /* + * Allow multiple versions of Raven to be installed. + * Strip Raven from the global context and returns the instance. + */ + noConflict(): this; + + /** Configure Raven with a DSN and extra options */ + config(dsn: string, options?: RavenOptions): this; + + /* + * Installs a global window.onerror error handler + * to capture and report uncaught exceptions. + * At this point, install() is required to be called due + * to the way TraceKit is set up. + */ + install(): this; + + /** Adds a plugin to Raven */ + addPlugin(plugin: RavenPlugin, ...pluginArgs: any[]): this; + + /* + * Wrap code within a context so Raven can capture errors + * reliably across domains that is executed immediately. + */ + context(func: Function, ...args: any[]): void; + context(options: RavenOptions, func: Function, ...args: any[]): void; + + /** Wrap code within a context and returns back a new function to be executed */ + wrap(func: Function): Function; + wrap(options: RavenOptions, func: Function): Function; + wrap(func: T): T; + wrap(options: RavenOptions, func: T): T; + + /** Uninstalls the global error handler. */ + uninstall(): this; + + /** Manually capture an exception and send it over to Sentry */ + captureException(ex: Error, options?: RavenOptions): this; + + /** Manually send a message to Sentry */ + captureMessage(msg: string, options?: RavenOptions): this; + + /** Log a breadcrumb */ + captureBreadcrumb(crumb: Breadcrumb): this; + + /** + * Clear the user context, removing the user data that would be sent to Sentry. + */ + setUserContext(): this; + + /** Set a user to be sent along with the payload. */ + setUserContext(user: { + id?: string, + username?: string, + email?: string, + }): this; + + /** Merge extra attributes to be sent along with the payload. */ + setExtraContext(context: Object): this; + + /** Merge tags to be sent along with the payload. */ + setTagsContext(tags: Object): this; + + /** Clear all of the context. */ + clearContext(): this; + + /** Get a copy of the current context. This cannot be mutated.*/ + getContext(): Object; + + /** Override the default HTTP data transport handler. */ + setTransport(transportFunction: (options: RavenTransportOptions) => void): this; + + /** Set environment of application */ + setEnvironment(environment: string): this; + + /** Set release version of application */ + setRelease(release: string): this; + + /** Get the latest raw exception that was captured by Raven.*/ + lastException(): Error; + + /** An event id is a globally unique id for the event that was just sent. This event id can be used to find the exact event from within Sentry. */ + lastEventId(): string; + + /** If you need to conditionally check if raven needs to be initialized or not, you can use the isSetup function. It will return true if Raven is already initialized. */ + isSetup(): boolean; + + /** Specify a function that allows mutation of the data payload right before being sent to Sentry. */ + setDataCallback(data: any, orig?: any): this; + + /** Specify a callback function that allows you to mutate or filter breadcrumbs when they are captured. */ + setBreadcrumbCallback(data: any, orig?: any): this; + + /** Specify a callback function that allows you to apply your own filters to determine if the message should be sent to Sentry. */ + setShouldSendCallback(data: any, orig?: any): this; + + /** Show Sentry user feedback dialog */ + showReportDialog(options: Object): void; + + /** Configure Raven DSN */ + setDSN(dsn: string): void; + } + + declare export default Raven +} diff --git a/package.json b/package.json index ece560e47d..ec77f024a4 100644 --- a/package.json +++ b/package.json @@ -62,10 +62,11 @@ "moment": "^2.18.1", "prop-types": "^15.5.10", "query-string": "^5.0.0", + "raven-js": "^3.22.1", "react": "^16.0.0", "react-dimensions": "^1.3.0", "react-dom": "^16.0.0", - "react-ga": "^2.2.0", + "react-ga": "^2.4.1", "react-helmet": "^5.1.3", "react-icons": "^2.2.7", "react-metrics": "^2.3.2", @@ -88,8 +89,8 @@ }, "scripts": { "start": "react-app-rewired start", - "start:docs": "REACT_APP_DEMO=true react-scripts start", - "build": "react-app-rewired build", + "start:ga-debug": "REACT_APP_GA_DEBUG=1 REACT_APP_VSN_STATE=$(./scripts/get-tracking-version.js) react-app-rewired start", + "build": "REACT_APP_VSN_STATE=$(./scripts/get-tracking-version.js) react-app-rewired build", "eject": "react-scripts eject", "test": "CI=1 react-app-rewired test --env=jsdom --color", "test-dev": "react-app-rewired test --env=jsdom", diff --git a/scripts/deploy-docs.sh b/scripts/deploy-docs.sh deleted file mode 100755 index b134956481..0000000000 --- a/scripts/deploy-docs.sh +++ /dev/null @@ -1,8 +0,0 @@ -# from the create-react-app post-build message: -git commit -am "Save local changes" -git checkout -B gh-pages -git add -f build -git commit -am "Rebuild website" -git filter-branch -f --prune-empty --subdirectory-filter build -git push -f origin gh-pages -git checkout - diff --git a/scripts/get-tracking-version.js b/scripts/get-tracking-version.js new file mode 100755 index 0000000000..17802f1ac2 --- /dev/null +++ b/scripts/get-tracking-version.js @@ -0,0 +1,92 @@ +#!/usr/bin/env node + +const spawnSync = require('child_process').spawnSync; + +const version = require('../package.json').version; + +function cleanRemoteUrl(url) { + return url.replace(/^(.*?@|.*?\/\/)|\.git\s*$/gi, '').replace(/:/g, '/'); +} + +function cleanBranchNames(pointsAt) { + const branch = pointsAt.replace(/"/g, '').split('\n')[0]; + const i = branch.indexOf(' '); + const objName = branch.slice(0, i); + let refName = branch.slice(i + 1); + if (refName.indexOf('detached') > -1) { + refName = '(detached)'; + } + return { objName, refName }; +} + +function getChanged(shortstat, status) { + const rv = { hasChanged: false, files: 0, insertions: 0, deletions: 0, untracked: 0 }; + const joiner = []; + const regex = /(\d+) (.)/g; + let match = regex.exec(shortstat); + while (match) { + const [, n, type] = match; + switch (type) { + case 'f': + rv.files = Number(n); + joiner.push(`${n}f`); + break; + case 'i': + rv.insertions = Number(n); + joiner.push(`+${n}`); + break; + case 'd': + rv.deletions = Number(n); + joiner.push(`-${n}`); + break; + default: + throw new Error(`Invalid diff type: ${type}`); + } + match = regex.exec(shortstat); + } + const untracked = status && status.split('\n').filter(line => line[0] === '?').length; + if (untracked) { + rv.untracked = untracked; + joiner.push(`${untracked}?`); + } + rv.pretty = joiner.join(' '); + rv.hasChanged = Boolean(joiner.length); + return rv; +} + +function getVersion(cwd) { + const opts = { cwd, encoding: 'utf8' }; + const url = spawnSync('git', ['remote', 'get-url', '--push', 'origin'], opts).stdout; + const branch = spawnSync( + 'git', + ['branch', '--points-at', 'HEAD', '--format="%(objectname:short) %(refname:short)"'], + opts + ).stdout; + const shortstat = spawnSync('git', ['diff-index', '--shortstat', 'HEAD'], opts).stdout; + const status = spawnSync('git', ['status', '--porcelain', '-uall'], opts).stdout; + + const { objName, refName } = cleanBranchNames(branch); + const remote = cleanRemoteUrl(url); + const joiner = [version, remote, objName]; + const changed = getChanged(shortstat, status); + if (changed.hasChanged) { + joiner.push(changed.pretty); + } + joiner.push(refName); + const rv = { + version, + remote, + objName, + changed, + refName, + pretty: joiner.join(' | '), + }; + return rv; +} + +if (require.main === module) { + const vsn = getVersion(process.argv[2] || '.'); + process.stdout.write(JSON.stringify(vsn)); +} else { + module.exports = getVersion; +} diff --git a/src/components/App/Page.js b/src/components/App/Page.js index 79854f69fc..53cca06ec1 100644 --- a/src/components/App/Page.js +++ b/src/components/App/Page.js @@ -23,7 +23,7 @@ import { withRouter } from 'react-router-dom'; import TopNav from './TopNav'; import type { Config } from '../../types/config'; -import { trackPageView } from '../../utils/metrics'; +import { trackPageView } from '../../utils/tracking'; import './Page.css'; diff --git a/src/components/App/Page.test.js b/src/components/App/Page.test.js index a0f2d8f65c..6761b444e8 100644 --- a/src/components/App/Page.test.js +++ b/src/components/App/Page.test.js @@ -14,13 +14,13 @@ /* eslint-disable import/first */ jest.mock('./TopNav', () => () =>
); -jest.mock('../../utils/metrics'); +jest.mock('../../utils/tracking'); import React from 'react'; import { mount } from 'enzyme'; import { mapStateToProps, PageImpl as Page } from './Page'; -import { trackPageView } from '../../utils/metrics'; +import { trackPageView } from '../../utils/tracking'; describe('mapStateToProps()', () => { it('maps state to props', () => { diff --git a/src/index.js b/src/index.js index 004ee47082..ede02d5c24 100644 --- a/src/index.js +++ b/src/index.js @@ -19,7 +19,7 @@ import ReactDOM from 'react-dom'; import { document } from 'global'; import JaegerUIApp from './components/App'; -import { init as initTracking } from './utils/metrics'; +import { context as trackingContext } from './utils/tracking'; import 'u-basscss/css/flexbox.css'; import 'u-basscss/css/layout.css'; @@ -28,11 +28,15 @@ import 'u-basscss/css/padding.css'; import 'u-basscss/css/position.css'; import 'u-basscss/css/typography.css'; -initTracking(); - const UI_ROOT_ID = 'jaeger-ui-root'; /* istanbul ignore if */ if (document && process.env.NODE_ENV !== 'test') { - ReactDOM.render(, document.getElementById(UI_ROOT_ID)); + if (trackingContext) { + trackingContext.context(() => { + ReactDOM.render(, document.getElementById(UI_ROOT_ID)); + }); + } else { + ReactDOM.render(, document.getElementById(UI_ROOT_ID)); + } } diff --git a/src/utils/metrics.js b/src/utils/metrics.js deleted file mode 100644 index 6b443137af..0000000000 --- a/src/utils/metrics.js +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) 2017 Uber Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import ReactGA from 'react-ga'; - -import getConfig from './config/get-config'; - -export function init() { - const config = getConfig(); - if (process.env.NODE_ENV === 'production' && config.gaTrackingID) { - ReactGA.initialize(config.gaTrackingID); - } -} - -export function trackPageView(pathname, search) { - const pagePath = search ? `${pathname}?${search}` : pathname; - ReactGA.pageview(pagePath); -} diff --git a/src/utils/tracking/conv-raven-to-ga.js b/src/utils/tracking/conv-raven-to-ga.js new file mode 100644 index 0000000000..814b042398 --- /dev/null +++ b/src/utils/tracking/conv-raven-to-ga.js @@ -0,0 +1,187 @@ +// @flow + +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* eslint-disable camelcase */ + +import prefixUrl from '../prefix-url'; + +const UNKONWN_SYM = { sym: '??', word: '??' }; + +const NAV_SYMBOLS = [ + { sym: 'dp', word: 'depdencies', rx: /^\/dep/i }, + { sym: 'tr', word: 'trace', rx: /^\/trace/i }, + { sym: 'sd', word: 'search', rx: /^\/search\?./i }, + { sym: 'sr', word: 'search', rx: /^\/search/i }, + { sym: 'rt', word: 'home', rx: /^\/$/ }, +]; + +const FETCH_SYMBOLS = [ + { sym: 'svc', word: '', rx: /^\/api\/services$/i }, + { sym: 'op', word: '', rx: /^\/api\/.*?operations$/i }, + { sym: 'sr', word: '', rx: /^\/api\/traces\?/i }, + { sym: 'tr', word: '', rx: /^\/api\/traces\/./i }, + { sym: 'dp', word: '', rx: /^\/api\/dep/i }, + { sym: '__IGNORE__', word: '', rx: /\.js(\.map)?$/i }, +]; + +// eslint-disable-next-line no-console +const warn = console.warn.bind(console); + +const origin = window.location.origin + prefixUrl(''); + +function truncate(str, len, front = false) { + if (str.length > len) { + if (!front) { + return `${str.slice(0, len - 1)}~`; + } + return `~${str.slice(1 - len)}`; + } + return str; +} + +function getSym(syms, str) { + for (let i = 0; i < syms.length; i++) { + const { rx } = syms[i]; + if (rx.test(str)) { + return syms[i]; + } + } + warn(`Unable to find symbol for: "${str}"`); + return UNKONWN_SYM; +} + +function convNav(to: string) { + const sym = getSym(NAV_SYMBOLS, to); + return `\n\n${sym.sym}\n`; +} + +function convFetch(data: { url: string, status_code: number }) { + const { url, status_code } = data; + const statusStr = status_code === 200 ? '' : `|${status_code}`; + const sym = getSym(FETCH_SYMBOLS, url); + if (sym.sym === '__IGNORE__') { + return ''; + } + return `[${sym.sym}${statusStr}]`; +} + +function convBreadcrumbs(crumbs) { + let joiner: string[] = []; + for (let i = 0; i < crumbs.length; i++) { + const c = crumbs[i]; + const cStart = c.category.split('.')[0]; + switch (cStart) { + case 'fetch': + joiner.push(convFetch(c.data)); + break; + + case 'ui': + joiner.push(c.category[3]); + break; + + case 'sentry': { + let msg = c.message; + const j = msg.indexOf(':'); + let start = msg.slice(0, j + 1); + start = start.replace(/error/gi, '').replace(':', '!'); + msg = truncate(`\n${start}${msg.slice(j + 1)}\n`, 60); + joiner.push(msg); + break; + } + + case 'navigation': + joiner.push(convNav(c.data.to)); + break; + + default: + // skip + } + } + joiner = joiner.filter(Boolean); + // combine repeating UI chars, e.g. ["c","c","c","c"] -> ["c","4"] + let c = ''; + let ci = -1; + const compacted = joiner.reduce((accum: string[], value: string, j: number): string[] => { + if (value === c) { + return accum; + } + if (c) { + if (j - ci > 1) { + accum.push(String(j - ci)); + } + c = ''; + ci = -1; + } + accum.push(value); + if (value.length === 1) { + c = value; + ci = j; + } + return accum; + }, []); + if (c && ci !== joiner.length - 1) { + compacted.push(String(joiner.length - 1 - ci)); + } + return compacted + .join('') + .trim() + .replace(/\n\n\n/g, '\n'); +} + +function convException(errValue) { + const type = errValue.type.replace(/error/gi, ''); + const message = truncate(`${type}! ${errValue.value}`, 149); + const frames = errValue.stacktrace.frames.map(fr => { + const filename = fr.filename.replace(origin, '').replace(/^\/static\/js\//i, ''); + const fn = fr.function; + return { filename, fn }; + }); + const joiner = []; + let lastFile = ''; + for (let i = frames.length - 1; i >= 0; i--) { + const { filename, fn } = frames[i]; + if (lastFile !== filename) { + joiner.push(`> ${filename}`); + lastFile = filename; + } + joiner.push(fn); + } + return { message, stack: joiner.join('\n') }; +} + +function getLabel(message, page, duration, git, crumbs) { + const header = [message, page, duration, git].filter(Boolean).join('\n'); + return `${header}\n${truncate(crumbs, 498 - header.length, true)}`; +} + +export default function convRavenToGa({ data }: RavenTransportOptions) { + const { message, stack } = convException(data.exception.values[0]); + const url = truncate(data.request.url.replace(origin, ''), 50); + const { word: page } = getSym(NAV_SYMBOLS, url); + const crumbs = convBreadcrumbs(data.breadcrumbs.values); + const value = Math.round(data.extra['session:duration'] / 1000); + const category = `jaeger/${page}/error`; + let action = [message, url, stack].join('\n'); + action = truncate(action, 499); + const label = getLabel(message, page, value, data.tags.git, crumbs); + return { + message, + category, + action, + label, + value, + }; +} diff --git a/src/utils/tracking/index.js b/src/utils/tracking/index.js new file mode 100644 index 0000000000..eff14a9ce3 --- /dev/null +++ b/src/utils/tracking/index.js @@ -0,0 +1,185 @@ +// @flow + +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import _get from 'lodash/get'; +import queryString from 'query-string'; +import ReactGA from 'react-ga'; +import Raven from 'raven-js'; + +import convRavenToGa from './conv-raven-to-ga'; +import getConfig from '../config/get-config'; + +type EventData = { + category: string, + action?: string, + label?: string, + value?: number, +}; + +const EVENT_LENGTHS = { + action: 499, + category: 149, + label: 499, +}; + +// Util so "0" and "false" become false +const isTruish = value => Boolean(value) && value !== '0' && value !== 'false'; + +const isProd = process.env.NODE_ENV === 'production'; +const isDev = process.env.NODE_ENV === 'development'; + +// In test mode if development and envvar REACT_APP_GA_DEBUG is true-ish +const isDebugMode = + (isDev && isTruish(process.env.REACT_APP_GA_DEBUG)) || + isTruish(queryString.parse(_get(window, 'location.search'))['ga-debug']); + +const config = getConfig(); +// enable for debug or if in prod with a GA ID +const isGaEnabled = isDebugMode || (isProd && Boolean(config.gaTrackingID)); + +let appVersion; +if (process.env.REACT_APP_VSN_STATE) { + try { + appVersion = JSON.parse(process.env.REACT_APP_VSN_STATE); + const joiner = [appVersion.objName]; + if (appVersion.changed.hasChanged) { + joiner.push(appVersion.changed.pretty); + } + appVersion.shortPretty = joiner.join(' '); + } catch (_) { + appVersion = { + pretty: process.env.REACT_APP_VSN_STATE, + shortPretty: process.env.REACT_APP_VSN_STATE, + }; + } +} else { + appVersion = { + pretty: 'unknown', + shortPretty: 'unknown', + }; +} + +function logTrackingCalls() { + const calls = ReactGA.testModeAPI.calls; + for (let i = 0; i < calls.length; i++) { + // eslint-disable-next-line no-console + console.log('[react-ga]', ...calls[i]); + } + calls.length = 0; +} + +export function trackPageView(pathname: string, search: ?string) { + if (isGaEnabled) { + const pagePath = search ? `${pathname}${search}` : pathname; + ReactGA.pageview(pagePath); + if (isDebugMode) { + logTrackingCalls(); + } + } +} + +export function trackError(description: string) { + if (isGaEnabled) { + let msg = description; + if (!/^jaeger/i.test(msg)) { + msg = `jaeger/${msg}`.slice(0, 149); + } + ReactGA.exception({ description: msg, fatal: false }); + if (isDebugMode) { + logTrackingCalls(); + } + } +} + +export function trackEvent(data: EventData) { + if (isGaEnabled) { + const event = {}; + let category = data.category; + if (!category) { + category = 'jaeger/event'; + } else if (!/^jaeger/i.test(category)) { + category = `jaeger/${category}`.slice(0, EVENT_LENGTHS.category); + } else { + category = category.slice(0, EVENT_LENGTHS.category); + } + event.category = category; + if (data.action) { + event.action = data.action.slice(0, EVENT_LENGTHS.action); + } + if (data.label) { + event.label = data.label.slice(0, EVENT_LENGTHS.label); + } + if (data.value != null) { + event.value = Number(data.value); + } + ReactGA.event(event); + if (isDebugMode) { + logTrackingCalls(); + } + } +} + +function trackRavenError(ravenData: RavenTransportOptions) { + const { message, ...gaData } = convRavenToGa(ravenData); + if (isDebugMode) { + Object.keys(gaData).forEach(key => { + // eslint-disable-next-line no-console + console.log(key); + // eslint-disable-next-line no-console + console.log(gaData[key]); + }); + } + trackError(message); + trackEvent(gaData); +} + +// Tracking needs to be initialized when this file is imported, e.g. early in +// the process of initializing the app, so Raven can wrap various resources, +// like `fetch()`, and generate breadcrumbs from them. + +if (isGaEnabled) { + const abbr = appVersion.pretty.length > 99 ? `${appVersion.pretty.slice(0, 96)}...` : appVersion.pretty; + const gaConfig = { testMode: isDebugMode, titleCase: false }; + ReactGA.initialize(config.gaTrackingID || 'debug-mode', gaConfig); + ReactGA.set({ + appId: 'github.com/jaegertracing/jaeger-ui', + appName: 'Jaeger UI', + appVersion: abbr, + }); + const ravenConfig = { + autoBreadcrumbs: { + xhr: true, + console: false, + dom: true, + location: true, + }, + environment: process.env.NODE_ENV || 'unkonwn', + transport: trackRavenError, + tags: {}, + }; + if (appVersion.shortPretty && appVersion.shortPretty !== 'unknown') { + ravenConfig.tags.git = appVersion.shortPretty; + } + Raven.config('https://fakedsn@omg.com/1', ravenConfig).install(); + window.onunhandledrejection = function trackRejectedPromise(evt) { + Raven.captureException(evt.reason); + }; + if (isDebugMode) { + logTrackingCalls(); + } +} + +export const context = isGaEnabled ? Raven : null; diff --git a/yarn.lock b/yarn.lock index 1ab7c4cb92..72604012ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6501,6 +6501,10 @@ range-parser@^1.0.3, range-parser@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" +raven-js@^3.22.1: + version "3.22.1" + resolved "https://registry.yarnpkg.com/raven-js/-/raven-js-3.22.1.tgz#1117f00dfefaa427ef6e1a7d50bbb1fb998a24da" + raw-body@2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.2.tgz#bcd60c77d3eb93cde0050295c3f379389bc88f89" @@ -6890,13 +6894,12 @@ react-error-overlay@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-4.0.0.tgz#d198408a85b4070937a98667f500c832f86bd5d4" -react-ga@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/react-ga/-/react-ga-2.2.0.tgz#45235de1356e4d988d9b82214d615a08489c9291" - dependencies: - create-react-class "^15.5.2" - object-assign "^4.0.1" - prop-types "^15.5.6" +react-ga@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/react-ga/-/react-ga-2.4.1.tgz#dfbd5f028ed39a07067f7a8bf57dc0d240000767" + optionalDependencies: + prop-types "^15.6.0" + react "^15.6.2 || ^16.0" react-helmet@^5.1.3: version "5.1.3" @@ -7128,7 +7131,7 @@ react-vis@^1.7.2: react-motion "^0.4.8" react-test-renderer "^15.5.4" -react@^16.0.0: +"react@^15.6.2 || ^16.0", react@^16.0.0: version "16.2.0" resolved "https://registry.yarnpkg.com/react/-/react-16.2.0.tgz#a31bd2dab89bff65d42134fa187f24d054c273ba" dependencies: From 2321117cc6e7db54b399fdff3db2d076851c2ac5 Mon Sep 17 00:00:00 2001 From: Joe Farro Date: Sun, 11 Feb 2018 13:49:21 -0500 Subject: [PATCH 02/18] Include CSS selector with last error breadcrumb Signed-off-by: Joe Farro --- src/utils/tracking/conv-raven-to-ga.js | 212 ++++++++++++++++++++----- src/utils/tracking/index.js | 17 +- 2 files changed, 186 insertions(+), 43 deletions(-) diff --git a/src/utils/tracking/conv-raven-to-ga.js b/src/utils/tracking/conv-raven-to-ga.js index 814b042398..7ff49f6e67 100644 --- a/src/utils/tracking/conv-raven-to-ga.js +++ b/src/utils/tracking/conv-raven-to-ga.js @@ -21,7 +21,7 @@ import prefixUrl from '../prefix-url'; const UNKONWN_SYM = { sym: '??', word: '??' }; const NAV_SYMBOLS = [ - { sym: 'dp', word: 'depdencies', rx: /^\/dep/i }, + { sym: 'dp', word: 'dependencies', rx: /^\/dep/i }, { sym: 'tr', word: 'trace', rx: /^\/trace/i }, { sym: 'sd', word: 'search', rx: /^\/search\?./i }, { sym: 'sr', word: 'search', rx: /^\/search/i }, @@ -40,8 +40,10 @@ const FETCH_SYMBOLS = [ // eslint-disable-next-line no-console const warn = console.warn.bind(console); +// common aspect of local URLs const origin = window.location.origin + prefixUrl(''); +// truncate and use "~" instead of ellipsis bc it's shorter function truncate(str, len, front = false) { if (str.length > len) { if (!front) { @@ -52,6 +54,7 @@ function truncate(str, len, front = false) { return str; } +// shorten URLs to eitehr a short code or a word function getSym(syms, str) { for (let i = 0; i < syms.length; i++) { const { rx } = syms[i]; @@ -63,49 +66,192 @@ function getSym(syms, str) { return UNKONWN_SYM; } +// Convert an exception to the error message and a compacted stack trace. The +// message is truncated to 149 characters. The strack trace is compacted to the +// following format: +// From (array of objects): +// { filename: 'http://origin/static/js/main.js', function: 'aFn' } +// { filename: 'http://origin/static/js/main.js', function: 'bFn' } +// { filename: 'http://origin/static/js/chunk.js', function: 'cFn' } +// { filename: 'http://origin/static/js/chunk.js', function: 'dFn' } +// To (string): +// > main.js +// aFn +// bFn +// > chunk.js +// cFn +// dFn +function convException(errValue) { + const type = errValue.type.replace(/error/gi, ''); + const message = truncate(`${type}! ${errValue.value}`, 149); + const frames = errValue.stacktrace.frames.map(fr => { + const filename = fr.filename.replace(origin, '').replace(/^\/static\/js\//i, ''); + const fn = fr.function; + return { filename, fn }; + }); + const joiner = []; + let lastFile = ''; + for (let i = frames.length - 1; i >= 0; i--) { + const { filename, fn } = frames[i]; + if (lastFile !== filename) { + joiner.push(`> ${filename}`); + lastFile = filename; + } + joiner.push(fn); + } + return { message, stack: joiner.join('\n') }; +} + +// Convert a navigation breadcrumb to one of the following string tokens: +// "dp" - dependencies page +// "tr" - trace page +// "sd" - search page with search results +// "sr" - search page +// "rt" - the root page function convNav(to: string) { const sym = getSym(NAV_SYMBOLS, to); - return `\n\n${sym.sym}\n`; + return sym.sym; } +// Convert a HTTP fetch breadcrumb to a string token in one of the two +// following forms: +// "[SYM]" +// "[SYM|NNN]" +// Where "SYM" is one of: +// "svc" - fetch the services for the search page +// "op" - fetch the operations for a service +// "sr" - execute a search +// "tr" - fetch a trace +// "dp" - fetch the dependency data +// And, "NNN" is a non-200 status code. function convFetch(data: { url: string, status_code: number }) { const { url, status_code } = data; const statusStr = status_code === 200 ? '' : `|${status_code}`; const sym = getSym(FETCH_SYMBOLS, url); if (sym.sym === '__IGNORE__') { - return ''; + return null; } return `[${sym.sym}${statusStr}]`; } +// Reduce the selector to something similar, but more compact. This is an +// informal reduction, i.e. the selector may actually function completely +// differently, but it should suffice as a reference for UI events. The +// intention is to trim the selector to something more compact but still +// recognizable. +// +// Some examples of the conversion: +// +// div.ub-relative. > span > span.detail-row-expanded-accent +// => .detail-row-expanded-accent +// +// header > div.TracePageHeader--titleRow > button.ant-btn.ub-mr2[type="button"] +// => .TracePageHeader--titleRow >.ant-btn[type="button"] +// +// span.SpanTreeOffset.is-parent > span.SpanTreeOffset--iconWrapper +// => .SpanTreeOffset.is-parent >.SpanTreeOffset--iconWrapper +// +// div > div > div.AccordianLogs > a.AccordianLogs--header. +// => .AccordianLogs >.AccordianLogs--header +// +// body > div > div > div.ant-modal-wrap. +// => .ant-modal-wrap +// +// a.ub-flex-auto.ub-mr2 > h1.TracePageHeader--title +// => .TracePageHeader--title +function compressCssSelector(selector) { + return ( + selector + // cut dangling dots, "div. > div" to "div > div" + .replace(/\.(?=\s|$)/g, '') + // cut ub-* class names, "a.ub-p.is-ok" to "a.is-ok" + .replace(/\.ub-[^. [:]+/g, '') + // cut leading tags, "div > a > .cls" to ".cls" + .replace(/^(\w+ > )+/, '') + // cut tag names when there is also a class, "a.is-ok" to ".is-ok" + .replace(/(^| )\w+?(?=\.)/g, '$1') + // cut the first space in child selectors, ".is-ok > .yuh" to ".is-ok >.yuh" + .replace(/ > /g, ' >') + ); +} + +// Convert the breadcrumbs to a compact string, discarding quite a lot of +// information. +// +// Navigation and HTTP fetch breadcrumbs are described above in `convFetch()` +// and `convNav()`. +// +// Previously logged errors captured by sentry are truncated to 58 characters +// and placed on their own line. Further, the first occurrence of "error" is +// removed and the first ":" is replaced with "!". E.g. the message: +// "Error: some error here with a very long message that will be truncated" +// Becomes: +// "\n! some error here with a very long message that will be t~\n" +// +// UI breadcrumbs are reduced to the first letter after the "ui.". And, +// repeated tokens are compacted to the form: +// "tN" +// Where "t" is the event type ("c" is click, "i" is input) and "N" is the +// total number of times it occured in that sequence. E.g. "c2" indicates +// two "ui.click" breadcrumbs. +// +// The chronological ordering of the breadcrumbs is older events precede newer +// events. This ordering was kept because it's easier to see which page events +// occurred on. function convBreadcrumbs(crumbs) { + // the last UI breadcrumb has the CSS selector included + let iLastUi = -1; + for (let i = crumbs.length - 1; i >= 0; i--) { + if (crumbs[i].category.slice(0, 2) === 'ui') { + iLastUi = i; + break; + } + } let joiner: string[] = []; + // note when we're on a newline to avoid extra newlines + let onNewLine = true; for (let i = 0; i < crumbs.length; i++) { const c = crumbs[i]; const cStart = c.category.split('.')[0]; switch (cStart) { - case 'fetch': - joiner.push(convFetch(c.data)); + case 'fetch': { + const fetched = convFetch(c.data); + if (fetched) { + joiner.push(fetched); + onNewLine = false; + } break; + } + + case 'navigation': { + const nav = `${onNewLine ? '' : '\n'}\n${convNav(c.data.to)}\n`; + joiner.push(nav); + onNewLine = true; + break; + } - case 'ui': - joiner.push(c.category[3]); + case 'ui': { + if (i === iLastUi) { + const selector = compressCssSelector(c.message); + joiner.push(`${c.category[3]}{${selector}}`); + } else { + joiner.push(c.category[3]); + } + onNewLine = false; break; + } case 'sentry': { let msg = c.message; const j = msg.indexOf(':'); - let start = msg.slice(0, j + 1); - start = start.replace(/error/gi, '').replace(':', '!'); - msg = truncate(`\n${start}${msg.slice(j + 1)}\n`, 60); + let start = msg.slice(0, j); + start = start.replace(/error/i, ''); + msg = `${onNewLine ? '' : '\n'}${truncate(`${start ? '! ' : ''}${start}!${msg.slice(j + 1)}`, 58)}\n`; joiner.push(msg); + onNewLine = true; break; } - case 'navigation': - joiner.push(convNav(c.data.to)); - break; - default: // skip } @@ -133,7 +279,7 @@ function convBreadcrumbs(crumbs) { return accum; }, []); if (c && ci !== joiner.length - 1) { - compacted.push(String(joiner.length - 1 - ci)); + compacted.push(String(joiner.length - ci)); } return compacted .join('') @@ -141,42 +287,26 @@ function convBreadcrumbs(crumbs) { .replace(/\n\n\n/g, '\n'); } -function convException(errValue) { - const type = errValue.type.replace(/error/gi, ''); - const message = truncate(`${type}! ${errValue.value}`, 149); - const frames = errValue.stacktrace.frames.map(fr => { - const filename = fr.filename.replace(origin, '').replace(/^\/static\/js\//i, ''); - const fn = fr.function; - return { filename, fn }; - }); - const joiner = []; - let lastFile = ''; - for (let i = frames.length - 1; i >= 0; i--) { - const { filename, fn } = frames[i]; - if (lastFile !== filename) { - joiner.push(`> ${filename}`); - lastFile = filename; - } - joiner.push(fn); - } - return { message, stack: joiner.join('\n') }; -} - -function getLabel(message, page, duration, git, crumbs) { - const header = [message, page, duration, git].filter(Boolean).join('\n'); +// Create the GA label value from the message, page, duration, git info, and +// breadcrumbs. See <./README.md> for details. +function getLabel(message, page, duration, git, breadcrumbs) { + const header = [message, page, duration, git, ''].filter(v => v != null).join('\n'); + const crumbs = convBreadcrumbs(breadcrumbs); return `${header}\n${truncate(crumbs, 498 - header.length, true)}`; } +// Convert the Raven exception data to something that can be sent to Google +// Analytics. See <./README.md> for details. export default function convRavenToGa({ data }: RavenTransportOptions) { + console.log(data); const { message, stack } = convException(data.exception.values[0]); const url = truncate(data.request.url.replace(origin, ''), 50); const { word: page } = getSym(NAV_SYMBOLS, url); - const crumbs = convBreadcrumbs(data.breadcrumbs.values); const value = Math.round(data.extra['session:duration'] / 1000); const category = `jaeger/${page}/error`; - let action = [message, url, stack].join('\n'); + let action = [message, data.tags.git, url, '', stack].filter(v => v != null).join('\n'); action = truncate(action, 499); - const label = getLabel(message, page, value, data.tags.git, crumbs); + const label = getLabel(message, page, value, data.tags.git, data.breadcrumbs.values); return { message, category, diff --git a/src/utils/tracking/index.js b/src/utils/tracking/index.js index eff14a9ce3..e4ad981802 100644 --- a/src/utils/tracking/index.js +++ b/src/utils/tracking/index.js @@ -133,11 +133,24 @@ export function trackEvent(data: EventData) { } function trackRavenError(ravenData: RavenTransportOptions) { - const { message, ...gaData } = convRavenToGa(ravenData); + console.log('trace raven error:', ravenData); + let converted; + try { + converted = convRavenToGa(ravenData); + // const { message, ...gaData } = convRavenToGa(ravenData); + } catch (err) { + console.error(err); + throw err; + } + const { message, ...gaData } = converted; if (isDebugMode) { Object.keys(gaData).forEach(key => { // eslint-disable-next-line no-console - console.log(key); + let valueLen = ''; + if (typeof gaData[key] === 'string') { + valueLen = `- value length: ${gaData[key].length}`; + } + console.log(key, valueLen); // eslint-disable-next-line no-console console.log(gaData[key]); }); From 45e93aa80035a2f57e5501ac9ee37898bdb696dc Mon Sep 17 00:00:00 2001 From: Joe Farro Date: Sun, 11 Feb 2018 13:49:45 -0500 Subject: [PATCH 03/18] README for GA error tracking Signed-off-by: Joe Farro --- src/utils/tracking/README.md | 198 +++++++++++++++++++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 src/utils/tracking/README.md diff --git a/src/utils/tracking/README.md b/src/utils/tracking/README.md new file mode 100644 index 0000000000..80bd7ba445 --- /dev/null +++ b/src/utils/tracking/README.md @@ -0,0 +1,198 @@ +# Google Analytics (GA) Tracking In Jaeger UI + +Page-views and errors are tracked in production when a GA tracking ID is provided in the UI config. See the +[documentation](http://jaeger.readthedocs.io/en/latest/deployment/#ui-configuration) for details on the UI +config. + +The page-view tracking is pretty basic. The error tracking is described, below. + +## Error Tracking + +Raven.js is used to capture error data ([GitHub](https://github.com/getsentry/raven-js), +[docs](https://docs.sentry.io/clients/javascript/)). Once captured, the error data is transformed and sent to +GA. + +### How Are Errors Being Tracked In GA? + +For every error we learn of, two GA calls are issued: + +* An [exception](https://developers.google.com/analytics/devguides/collection/analyticsjs/exceptions) +* An [event](https://developers.google.com/analytics/devguides/collection/analyticsjs/events) + +GA exception tracking is pretty minimal, allowing just a +[150 byte string](https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#exDescription). +So, in addition to the exception, an event with additional data is also issued. + +* [Category](https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#eventCategory) - + The page type the error occurred on +* [Action](https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#eventAction) - + Error information with a compacted stack trace (sans sourcemaps, at this time) +* [Label](https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#eventLabel) - + A compact form of the breadcrumbs +* [Value](https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#eventValue) - + The duration of the session when the error occurred (in seconds) + +#### Event: Category - Which Page + +The category indicates which type of page the error occurred on, and will be one of the following: + +* `jaeger/home/error` - The site root +* `jaeger/search/error` - The search page +* `jaeger/trace/error` - The trace page +* `jaeger/dependencies/error` - The Dependencies page + +#### Event: Action - Error Info + +The action contains: + +* The error message (truncated to 149 characters) +* A compact form of the git status (SHA and diff shortstat) +* The page URL with the origin and path-prefix removed (truncated to 50 characters) +* A compact form of the stack trace (without the benefit of sourcemaps, at this time) + +For example, the following error: + +``` +Error: test-sentry + at o (main.1ae26b34.js:1) + at e.value (main.1ae26b34.js:1) + at scrollToNextVisibleSpan (main.1ae26b34.js:1) + at e.exports [as fireCallback] (main.1ae26b34.js:1) + at e.exports [as handleKey] (main.1ae26b34.js:1) + at e.exports (chunk.6b341ae2.js:1) + at HTMLBodyElement.r (chunk.6b341ae2.js:1) +``` + +Might be tracked as the following event action (without the comments): + +``` +! test-sentry # error message +/trace/abc123def # relevant portion of the URL +8c50c6c 2f +33 -56 1? # commit SHA and 2 edited files, 1 unknown file + # stack trace starts +> main.1ae26b34.js # source file for the following frames +o # function `o` in main.1ae26b34.js +e.value # function `e.value` in main.1ae26b34.js +scrollToNextVisibleSpan # etc... +e.exports [as fireCallback] +e.exports [as handleKey] +> chunk.6b341ae2.js # source file for the following frames +e.exports # function `e.exports` in chunk.6b341ae2.js +HTMLBodyElement.r # also in chunk.6b341ae2.js +``` + +The `+33 -56` means there are 33 inserted lines and 56 deleted lines in the edits made to the two tracked +files. + +Note: The git status is determined when the build is generated or when `yarn start` is initially executed to +start the dev server. + +#### Event: Label - Breadcrumbs + +The label contains: + +* The error message (truncated to 149 characters) +* The type of page the error occurred on +* The duration, in seconds, of the session when the error occurred +* A compact form of the git status (SHA and diff shortstat) +* A compact form of breadcrumbs, with older entries preceding newer entries + +For example, the following label: + +``` +! Houston we have a problem +trace +18 +8c50c6c 2f +34 -56 1? + +[tr|404] + +sr +[svc][op]cic + +sd +[sr]c3 + +tr +cc{.SpanTreeOffset.is-parent >.SpanTreeOffset--iconWrapper} +! test-sentry +``` + +Indicates: + +* The error message is: `Error: Houston we have a problem` +* The error occurred on the trace page +* The error occurred 18 seconds into the session +* The build was generated from commit `8c50c6c` with two modified files and one untracked file +* The sequence of events indicated by the breadcrumbs is (oldest to most recent): + * On the first page of the session + * `[tr|404]` - A HTTP call to fetch a trace returned a `404` status code + * `sr` - Next, on the search page + * `[svc]` - The services were fetched with a `200` status code + * `[op]` - The operations for a service were fetched with a `200` status code + * `c` - 1 click + * `i` - 1 text input + * `c` - 1 click + * `sd` - Next, on a search page showing results + * `[sr]` - A HTTP call to execute a search returned a `200` status code + * `c3` - 3 click UI interactions + * `tr` - Next, on a trace page + * `cc` - 2 clicks + * `c{.SpanTree...}` - The second click is the last UI breadcrumb, so it is shown with a CSS selector + related to the click event target. The CSS selector is "related" instead of "identifying" because it's + been simplified. + * `! test-sentry` - An error with the message `Error: test-sentry` + * The error being tracked occurred — implicit as the next event + +The cryptic encoding for the breadcrumbs is used to fit as much of the event history into the 500 characters +as possible. It might turn out that fewer events with more details is preferable. In which case, the payload +will be adjusted. For now, the encoding is: + +* `[sym]` - A fetch to `sym` resulted in a `200` status code, possible values for `sym` are: + * `svc` - Fetch the services for the search page + * `op` - Fetch the operations for a service + * `sr` - Execute a search + * `tr` - Fetch a trace + * `dp` - Fetch the dependency data + * `??` - Unknown fetch (should not happen) +* `[sym|NNN]` - The status code was `NNN`, omitted for `200` status codes +* `\n\nsym\n` - Navigation to `sym` + * Page navigation tokens are on their own line and have an empty line above them, e.g. empty lines separate + events that occurred on different pages + * `sym` indicates the type of page, valid values are: + * `dp` - Dependencies page + * `tr` - Trace page + * `sd` - Search page with search results + * `sr` - Search page + * `rt` - The root page + * `??` - Uknown page (should not happen) +* `c` or `i` - Indicates a user interaction + * `c` is click + * `i` is input + * `cN` - Indicates `c` occurred `N` consecutive times, e.g. 3 clicks would be `c3` and `i2` is two input + breadcrumbs + * `c{selector}` - Indicates `c` was the last UI breadcrumb, and the CSS selector `selector` describes the + event target + * Takes for the form `i{selector}` for input events +* `! ` - A previous error that was tracked, truncated to 58 characters + * The first occurrence of `/error/i` is removed + * The first `:` is replaced with `!` + +### [Sentry](https://github.com/getsentry) Is Not Being Used + +Using Sentry is currently under consideration. In the meantime, errors can be tracked with GA. + +### Why Use Raven.js + +You get a lot for free when using Raven.js: + +* [Breadcrumbs](https://docs.sentry.io/learn/breadcrumbs/), which include: + * [`fetch`](https://github.com/getsentry/raven-js/blob/master/src/raven.js#L1242) HTTP requests + * [Previous errors](https://github.com/getsentry/raven-js/blob/master/src/raven.js#L1872) + * Some [UI events](https://github.com/getsentry/raven-js/blob/master/src/raven.js#L870) (click and input) + * [URL changes](https://github.com/getsentry/raven-js/blob/master/src/raven.js#L945) +* Stack traces are + [normalized](https://github.com/getsentry/raven-js/blob/f8eec063c95f70d8978f895284946bd278748d97/vendor/TraceKit/tracekit.js) +* Some global handlers are added + +Implementing the above from scratch would require substantial effort. Meanwhile, Raven.js is well tested. From 11d2f037ef3802252b2f59b6157f056e44a3524d Mon Sep 17 00:00:00 2001 From: Joe Farro Date: Sun, 11 Feb 2018 14:33:03 -0500 Subject: [PATCH 04/18] Misc cleanup Signed-off-by: Joe Farro --- src/utils/tracking/conv-raven-to-ga.js | 1 - src/utils/tracking/index.js | 13 ++----------- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/src/utils/tracking/conv-raven-to-ga.js b/src/utils/tracking/conv-raven-to-ga.js index 7ff49f6e67..a914769078 100644 --- a/src/utils/tracking/conv-raven-to-ga.js +++ b/src/utils/tracking/conv-raven-to-ga.js @@ -298,7 +298,6 @@ function getLabel(message, page, duration, git, breadcrumbs) { // Convert the Raven exception data to something that can be sent to Google // Analytics. See <./README.md> for details. export default function convRavenToGa({ data }: RavenTransportOptions) { - console.log(data); const { message, stack } = convException(data.exception.values[0]); const url = truncate(data.request.url.replace(origin, ''), 50); const { word: page } = getSym(NAV_SYMBOLS, url); diff --git a/src/utils/tracking/index.js b/src/utils/tracking/index.js index e4ad981802..94e3213390 100644 --- a/src/utils/tracking/index.js +++ b/src/utils/tracking/index.js @@ -133,23 +133,14 @@ export function trackEvent(data: EventData) { } function trackRavenError(ravenData: RavenTransportOptions) { - console.log('trace raven error:', ravenData); - let converted; - try { - converted = convRavenToGa(ravenData); - // const { message, ...gaData } = convRavenToGa(ravenData); - } catch (err) { - console.error(err); - throw err; - } - const { message, ...gaData } = converted; + const { message, ...gaData } = convRavenToGa(ravenData); if (isDebugMode) { Object.keys(gaData).forEach(key => { - // eslint-disable-next-line no-console let valueLen = ''; if (typeof gaData[key] === 'string') { valueLen = `- value length: ${gaData[key].length}`; } + // eslint-disable-next-line no-console console.log(key, valueLen); // eslint-disable-next-line no-console console.log(gaData[key]); From 1f11a8f16c11f3a8c9f0a41513c26b354b6d363f Mon Sep 17 00:00:00 2001 From: Joe Farro Date: Sun, 11 Feb 2018 15:07:15 -0500 Subject: [PATCH 05/18] README info on GA Application Tracking Signed-off-by: Joe Farro --- src/utils/tracking/README.md | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/utils/tracking/README.md b/src/utils/tracking/README.md index 80bd7ba445..41d553a506 100644 --- a/src/utils/tracking/README.md +++ b/src/utils/tracking/README.md @@ -4,7 +4,31 @@ Page-views and errors are tracked in production when a GA tracking ID is provide [documentation](http://jaeger.readthedocs.io/en/latest/deployment/#ui-configuration) for details on the UI config. -The page-view tracking is pretty basic. The error tracking is described, below. +The page-view tracking is pretty basic, so details aren't provided. The GA tracking is configured with [App Tracking](https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#apptracking) data. These fields, described [below](#app-tracking), can be used as a secondary dimension when viewing event data in GA. The error tracking is described, [below](#error-tracking). + +## App Tracking + +The following fields are sent for each GA session: + +* [Application Name](https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#appName) + * Set to `Jaeger UI` +* [Application ID](https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#appId) + * Set to `github.com/jaegertracing/jaeger-ui` +* [Application Version](https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#appVersion) + * Example: `0.0.1 | github.com/jaegertracing/jaeger-ui | 8c50c6c | 2f +2 -12 | master` + * A dynamic value set to: ` | | | | ` + * Truncated to 96 characters + * **version** - `package.json#version` + * **git remote** - `git remote get-url --push origin`, normalized + * **short SHA** - `git branch --points-at HEAD --format="%(objectname:short)"` + * **diff shortstat** - A compacted `git diff-index --shortstat HEAD` + * E.g. `2f +3 -4 5?` + * 2 modified files, having + * 3 insertions and + * 4 deletions + * 5 untracked files + * **branch name** - `$ git branch --points-at HEAD --format="%(refname:short)"` + * `(detached)` is used when HEAD is detached because the SHA is already noted ## Error Tracking From d6bb7e28e1bcce85d016ef0f67176bf5cda293fa Mon Sep 17 00:00:00 2001 From: Joe Farro Date: Sun, 11 Feb 2018 15:15:19 -0500 Subject: [PATCH 06/18] Misc fix to tracking README Signed-off-by: Joe Farro --- src/utils/tracking/README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/utils/tracking/README.md b/src/utils/tracking/README.md index 41d553a506..8e3196eafa 100644 --- a/src/utils/tracking/README.md +++ b/src/utils/tracking/README.md @@ -144,10 +144,11 @@ cc{.SpanTreeOffset.is-parent >.SpanTreeOffset--iconWrapper} Indicates: -* The error message is: `Error: Houston we have a problem` -* The error occurred on the trace page -* The error occurred 18 seconds into the session -* The build was generated from commit `8c50c6c` with two modified files and one untracked file +* `! Houston...` - The error message is `Error: Houston we have a problem` +* `trace` - The error occurred on the trace page +* `18` - The error occurred 18 seconds into the session +* `8c50c6c 2f +34 -56 1?` - The build was generated from commit `8c50c6c` with two modified files and one + untracked file * The sequence of events indicated by the breadcrumbs is (oldest to most recent): * On the first page of the session * `[tr|404]` - A HTTP call to fetch a trace returned a `404` status code From b1a94a7fddeed0355a2dcfd823d1a5932d575004 Mon Sep 17 00:00:00 2001 From: Joe Farro Date: Sun, 11 Feb 2018 21:06:26 -0500 Subject: [PATCH 07/18] Misc cleanup to raven message conversion to GA Signed-off-by: Joe Farro --- src/utils/tracking/conv-raven-to-ga.js | 55 +++++++++++++++++++++----- 1 file changed, 46 insertions(+), 9 deletions(-) diff --git a/src/utils/tracking/conv-raven-to-ga.js b/src/utils/tracking/conv-raven-to-ga.js index a914769078..e179ff65e3 100644 --- a/src/utils/tracking/conv-raven-to-ga.js +++ b/src/utils/tracking/conv-raven-to-ga.js @@ -54,6 +54,15 @@ function truncate(str, len, front = false) { return str; } +// Replace newlines with "|" and collapse whitespace to " " +function collapseWhitespace(value) { + return value + .trim() + .replace(/\n/g, '|') + .replace(/\s\s+/g, ' ') + .trim(); +} + // shorten URLs to eitehr a short code or a word function getSym(syms, str) { for (let i = 0; i < syms.length; i++) { @@ -66,6 +75,39 @@ function getSym(syms, str) { return UNKONWN_SYM; } +// Convert an error message to a shorter string with the first "error" removed, +// a leading "! " added, and the first ":" replaced with "!". +// +// Error: Houston we have a problem +// ! Houston we have a problem +// +// Error: HTTP Error: Fetch failed +// ! HTTP Error: Fetch failed +// +// TypeError: Awful things are happening +// ! Type! Awful things are happening +// +// The real error message +// ! The real error message +function convErrorMessage(message, maxLen = 0) { + let msg = collapseWhitespace(message); + const parts = ['! ']; + const j = msg.indexOf(':'); + if (j > -1) { + const start = msg + .slice(0, j) + .replace(/error/i, '') + .trim(); + if (start) { + parts.push(start, '! '); + } + msg = msg.slice(j + 1); + } + parts.push(msg.trim()); + const rv = parts.join(''); + return maxLen ? truncate(rv, maxLen) : parts.join(''); +} + // Convert an exception to the error message and a compacted stack trace. The // message is truncated to 149 characters. The strack trace is compacted to the // following format: @@ -82,11 +124,10 @@ function getSym(syms, str) { // cFn // dFn function convException(errValue) { - const type = errValue.type.replace(/error/gi, ''); - const message = truncate(`${type}! ${errValue.value}`, 149); + const message = convErrorMessage(`${errValue.type}: ${errValue.value}`, 149); const frames = errValue.stacktrace.frames.map(fr => { const filename = fr.filename.replace(origin, '').replace(/^\/static\/js\//i, ''); - const fn = fr.function; + const fn = collapseWhitespace(fr.function); return { filename, fn }; }); const joiner = []; @@ -242,12 +283,8 @@ function convBreadcrumbs(crumbs) { } case 'sentry': { - let msg = c.message; - const j = msg.indexOf(':'); - let start = msg.slice(0, j); - start = start.replace(/error/i, ''); - msg = `${onNewLine ? '' : '\n'}${truncate(`${start ? '! ' : ''}${start}!${msg.slice(j + 1)}`, 58)}\n`; - joiner.push(msg); + const msg = convErrorMessage(c.message, 58); + joiner.push(`${onNewLine ? '' : '\n'}${msg}\n`); onNewLine = true; break; } From 8a90112c2cd6f40c095d66de7916bca770a6f923 Mon Sep 17 00:00:00 2001 From: Joe Farro Date: Mon, 12 Feb 2018 00:36:51 -0500 Subject: [PATCH 08/18] Tests for tracking Signed-off-by: Joe Farro --- src/utils/tracking/README.md | 5 +- src/utils/tracking/conv-raven-to-ga.test.js | 23 ++ src/utils/tracking/fixtures.js | 221 ++++++++++++++++++++ src/utils/tracking/index.js | 82 ++++---- src/utils/tracking/index.test.js | 124 +++++++++++ 5 files changed, 414 insertions(+), 41 deletions(-) create mode 100644 src/utils/tracking/conv-raven-to-ga.test.js create mode 100644 src/utils/tracking/fixtures.js create mode 100644 src/utils/tracking/index.test.js diff --git a/src/utils/tracking/README.md b/src/utils/tracking/README.md index 8e3196eafa..9aeddea76b 100644 --- a/src/utils/tracking/README.md +++ b/src/utils/tracking/README.md @@ -4,7 +4,10 @@ Page-views and errors are tracked in production when a GA tracking ID is provide [documentation](http://jaeger.readthedocs.io/en/latest/deployment/#ui-configuration) for details on the UI config. -The page-view tracking is pretty basic, so details aren't provided. The GA tracking is configured with [App Tracking](https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#apptracking) data. These fields, described [below](#app-tracking), can be used as a secondary dimension when viewing event data in GA. The error tracking is described, [below](#error-tracking). +The page-view tracking is pretty basic, so details aren't provided. The GA tracking is configured with +[App Tracking](https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#apptracking) +data. These fields, described [below](#app-tracking), can be used as a secondary dimension when viewing event +data in GA. The error tracking is described, [below](#error-tracking). ## App Tracking diff --git a/src/utils/tracking/conv-raven-to-ga.test.js b/src/utils/tracking/conv-raven-to-ga.test.js new file mode 100644 index 0000000000..58b6ef2661 --- /dev/null +++ b/src/utils/tracking/conv-raven-to-ga.test.js @@ -0,0 +1,23 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import convRavenToGa from './conv-raven-to-ga'; +import { RAVEN_PAYLOAD, RAVEN_TO_GA } from './fixtures'; + +describe('convRavenToGa()', () => { + it('converts the raven-js payload to { category, action, label, value }', () => { + const data = convRavenToGa(RAVEN_PAYLOAD); + expect(data).toEqual(RAVEN_TO_GA); + }); +}); diff --git a/src/utils/tracking/fixtures.js b/src/utils/tracking/fixtures.js new file mode 100644 index 0000000000..0f0c9f57b5 --- /dev/null +++ b/src/utils/tracking/fixtures.js @@ -0,0 +1,221 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import deepFreeze from 'deep-freeze'; + +const poeExcerpt = ` +Excerpt of Alone, by Edgar Allen Poe: +"Then—in my childhood—in the dawn +Of a most stormy life—was drawn +From the red cliff of the mountain— +From the sun that ’round me roll’d +In its autumn tint of gold— +From the lightning in the sky +As it pass’d me flying by— +From the thunder, and the storm +And the cloud that took the form +(When the rest of Heaven was blue) +Of a demon in my view—" +3/17/1829`; + +module.exports.RAVEN_PAYLOAD = deepFreeze({ + data: { + request: { + url: 'http://localhost/trace/565c1f00385ebd0b', + }, + exception: { + values: [ + { + type: 'Error', + value: 'test-sentry', + stacktrace: { + frames: [ + { + filename: 'http://localhost/static/js/ultra-long-func.js', + function: poeExcerpt, + }, + { + filename: 'http://localhost/static/js/b.js', + function: 'fnBb', + }, + { + filename: 'http://localhost/static/js/b.js', + function: 'fnBa', + }, + { + filename: 'http://localhost/static/js/a.js', + function: 'fnAb', + }, + { + filename: 'http://localhost/static/js/a.js', + function: 'fnAa', + }, + { + filename: 'http://localhost/static/js/a.js', + function: 'HTMLBodyElement.wrapped', + }, + ], + }, + }, + ], + }, + tags: { + git: 'SHA shortstat', + }, + extra: { + 'session:duration': 10952, + }, + breadcrumbs: { + values: [ + { + category: 'sentry', + message: '6 Breadcrumbs should be truncated from the top (oldest)', + }, + { + category: 'sentry', + message: '5 Breadcrumbs should be truncated from the top (oldest)', + }, + { + category: 'sentry', + message: '4 Breadcrumbs should be truncated from the top (oldest)', + }, + { + category: 'sentry', + message: '3 Breadcrumbs should be truncated from the top (oldest)', + }, + { + category: 'sentry', + message: '2 Breadcrumbs should be truncated from the top (oldest)', + }, + { + category: 'sentry', + message: '1 Breadcrumbs should be truncated from the top (oldest)', + }, + { + category: 'sentry', + message: '0 Breadcrumbs should be truncated from the top (oldest)', + }, + { + type: 'http', + category: 'fetch', + data: { + url: '/api/traces/565c1f00385ebd0b', + status_code: 200, + }, + }, + { + type: 'http', + category: 'fetch', + data: { + url: '/api/traces/565c1f00385ebd0b', + status_code: 404, + }, + }, + { + type: 'http', + category: 'fetch', + data: { + url: '/unknown/url/1', + status_code: 200, + }, + }, + { + category: 'navigation', + data: { + to: '/trace/cde2457775afa8d2', + }, + }, + { + category: 'navigation', + data: { + to: '/uknonwn/url', + }, + }, + { + category: 'sentry', + message: 'Error: test-sentry', + }, + { + category: 'sentry', + message: + "TypeError: A very long message that will be truncated and reduced to a faint flicker of it's former glory", + }, + { + category: 'ui.click', + }, + { + category: 'ui.input', + }, + { + category: 'ui.click', + }, + { + category: 'ui.click', + }, + { + category: 'ui.input', + }, + { + category: 'ui.input', + }, + { + category: 'ui.input', + message: 'header > ul.LabeledList.TracePageHeader--overviewItems', + }, + ], + }, + }, +}); + +const action = `! test-sentry +SHA shortstat +/trace/565c1f00385ebd0b + +> a.js +HTMLBodyElement.wrapped +fnAa +fnAb +> b.js +fnBa +fnBb +> ultra-long-func.js +Excerpt of Alone, by Edgar Allen Poe:|"Then—in my childhood—in the dawn|Of a most stormy life—was drawn|From the red cliff of the mountain—|From the sun that ’round me roll’d|In its autumn tint of gold—|From the lightning in the sky|As it pass’d me flying by—|From the thunder, and the storm|And the cloud that took the form|(When the rest of Heaven was blue)|Of a d~`; + +const label = `! test-sentry +trace +11 +SHA shortstat + +~om the top (oldest) +! 4 Breadcrumbs should be truncated from the top (oldest) +! 3 Breadcrumbs should be truncated from the top (oldest) +! 2 Breadcrumbs should be truncated from the top (oldest) +! 1 Breadcrumbs should be truncated from the top (oldest) +! 0 Breadcrumbs should be truncated from the top (oldest) +[tr][tr|404][??] + +tr + +?? +! test-sentry +! Type! A very long message that will be truncated and re~ +cic2i2i{.LabeledList.TracePageHeader--overviewItems}`; + +module.exports.RAVEN_TO_GA = deepFreeze({ + action, + label, + message: '! test-sentry', + category: 'jaeger/trace/error', + value: 11, +}); diff --git a/src/utils/tracking/index.js b/src/utils/tracking/index.js index 94e3213390..70ad413f2a 100644 --- a/src/utils/tracking/index.js +++ b/src/utils/tracking/index.js @@ -40,6 +40,7 @@ const isTruish = value => Boolean(value) && value !== '0' && value !== 'false'; const isProd = process.env.NODE_ENV === 'production'; const isDev = process.env.NODE_ENV === 'development'; +const isTest = process.env.NODE_ENV === 'test'; // In test mode if development and envvar REACT_APP_GA_DEBUG is true-ish const isDebugMode = @@ -47,31 +48,10 @@ const isDebugMode = isTruish(queryString.parse(_get(window, 'location.search'))['ga-debug']); const config = getConfig(); -// enable for debug or if in prod with a GA ID -const isGaEnabled = isDebugMode || (isProd && Boolean(config.gaTrackingID)); - -let appVersion; -if (process.env.REACT_APP_VSN_STATE) { - try { - appVersion = JSON.parse(process.env.REACT_APP_VSN_STATE); - const joiner = [appVersion.objName]; - if (appVersion.changed.hasChanged) { - joiner.push(appVersion.changed.pretty); - } - appVersion.shortPretty = joiner.join(' '); - } catch (_) { - appVersion = { - pretty: process.env.REACT_APP_VSN_STATE, - shortPretty: process.env.REACT_APP_VSN_STATE, - }; - } -} else { - appVersion = { - pretty: 'unknown', - shortPretty: 'unknown', - }; -} +// enable for tests, debug or if in prod with a GA ID +const isGaEnabled = isTest || isDebugMode || (isProd && Boolean(config.gaTrackingID)); +/* istanbul ignore next */ function logTrackingCalls() { const calls = ReactGA.testModeAPI.calls; for (let i = 0; i < calls.length; i++) { @@ -95,8 +75,9 @@ export function trackError(description: string) { if (isGaEnabled) { let msg = description; if (!/^jaeger/i.test(msg)) { - msg = `jaeger/${msg}`.slice(0, 149); + msg = `jaeger/${msg}`; } + msg = msg.slice(0, 149); ReactGA.exception({ description: msg, fatal: false }); if (isDebugMode) { logTrackingCalls(); @@ -116,9 +97,7 @@ export function trackEvent(data: EventData) { category = category.slice(0, EVENT_LENGTHS.category); } event.category = category; - if (data.action) { - event.action = data.action.slice(0, EVENT_LENGTHS.action); - } + event.action = data.action ? data.action.slice(0, EVENT_LENGTHS.action) : 'jaeger/action'; if (data.label) { event.label = data.label.slice(0, EVENT_LENGTHS.label); } @@ -133,21 +112,25 @@ export function trackEvent(data: EventData) { } function trackRavenError(ravenData: RavenTransportOptions) { - const { message, ...gaData } = convRavenToGa(ravenData); + const data = convRavenToGa(ravenData); if (isDebugMode) { - Object.keys(gaData).forEach(key => { + /* istanbul ignore next */ + Object.keys(data).forEach(key => { + if (key === 'message') { + return; + } let valueLen = ''; - if (typeof gaData[key] === 'string') { - valueLen = `- value length: ${gaData[key].length}`; + if (typeof data[key] === 'string') { + valueLen = `- value length: ${data[key].length}`; } // eslint-disable-next-line no-console console.log(key, valueLen); // eslint-disable-next-line no-console - console.log(gaData[key]); + console.log(data[key]); }); } - trackError(message); - trackEvent(gaData); + trackError(data.message); + trackEvent(data); } // Tracking needs to be initialized when this file is imported, e.g. early in @@ -155,13 +138,32 @@ function trackRavenError(ravenData: RavenTransportOptions) { // like `fetch()`, and generate breadcrumbs from them. if (isGaEnabled) { - const abbr = appVersion.pretty.length > 99 ? `${appVersion.pretty.slice(0, 96)}...` : appVersion.pretty; - const gaConfig = { testMode: isDebugMode, titleCase: false }; + let versionShort; + let versionLong; + if (process.env.REACT_APP_VSN_STATE) { + try { + const data = JSON.parse(process.env.REACT_APP_VSN_STATE); + const joiner = [data.objName]; + if (data.changed.hasChanged) { + joiner.push(data.changed.pretty); + } + versionShort = joiner.join(' '); + versionLong = data.pretty; + } catch (_) { + versionShort = process.env.REACT_APP_VSN_STATE; + versionLong = process.env.REACT_APP_VSN_STATE; + } + versionLong = versionLong.length > 99 ? `${versionLong.slice(0, 96)}...` : versionLong; + } else { + versionShort = 'unknown'; + versionLong = 'unknown'; + } + const gaConfig = { testMode: isTest || isDebugMode, titleCase: false }; ReactGA.initialize(config.gaTrackingID || 'debug-mode', gaConfig); ReactGA.set({ appId: 'github.com/jaegertracing/jaeger-ui', appName: 'Jaeger UI', - appVersion: abbr, + appVersion: versionLong, }); const ravenConfig = { autoBreadcrumbs: { @@ -174,8 +176,8 @@ if (isGaEnabled) { transport: trackRavenError, tags: {}, }; - if (appVersion.shortPretty && appVersion.shortPretty !== 'unknown') { - ravenConfig.tags.git = appVersion.shortPretty; + if (versionShort && versionShort !== 'unknown') { + ravenConfig.tags.git = versionShort; } Raven.config('https://fakedsn@omg.com/1', ravenConfig).install(); window.onunhandledrejection = function trackRejectedPromise(evt) { diff --git a/src/utils/tracking/index.test.js b/src/utils/tracking/index.test.js new file mode 100644 index 0000000000..3f4c59896d --- /dev/null +++ b/src/utils/tracking/index.test.js @@ -0,0 +1,124 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* eslint-disable import/first */ +jest.mock('./conv-raven-to-ga', () => () => ({ message: 'jaeger/a' })); + +jest.mock('./index', () => { + process.env.REACT_APP_VSN_STATE = '{}'; + return require.requireActual('./index'); +}); + +import ReactGA from 'react-ga'; + +import * as tracking from './index'; + +let longStr = '---'; +function getStr(len: number) { + while (longStr.length < len) { + longStr += longStr.slice(0, len - longStr.length); + } + return longStr.slice(0, len); +} + +describe('tracking', () => { + let calls; + + beforeEach(() => { + calls = ReactGA.testModeAPI.calls; + calls.length = 0; + }); + + describe('trackPageView', () => { + it('tracks a page view', () => { + tracking.trackPageView('a', 'b'); + expect(calls).toEqual([['send', { hitType: 'pageview', page: 'ab' }]]); + }); + + it('ignores search when it is falsy', () => { + tracking.trackPageView('a'); + expect(calls).toEqual([['send', { hitType: 'pageview', page: 'a' }]]); + }); + }); + + describe('trackError', () => { + it('tracks an error', () => { + tracking.trackError('a'); + expect(calls).toEqual([ + ['send', { hitType: 'exception', exDescription: jasmine.any(String), exFatal: false }], + ]); + }); + + it('ensures "jaeger" is prepended', () => { + tracking.trackError('a'); + expect(calls).toEqual([['send', { hitType: 'exception', exDescription: 'jaeger/a', exFatal: false }]]); + }); + + it('truncates if needed', () => { + const str = `jaeger/${getStr(200)}`; + tracking.trackError(str); + expect(calls).toEqual([ + ['send', { hitType: 'exception', exDescription: str.slice(0, 149), exFatal: false }], + ]); + }); + }); + + describe('trackEvent', () => { + it('tracks an event', () => { + tracking.trackEvent({ value: 10 }); + expect(calls).toEqual([ + [ + 'send', + { + hitType: 'event', + eventCategory: jasmine.any(String), + eventAction: jasmine.any(String), + eventValue: 10, + }, + ], + ]); + }); + + it('prepends "jaeger/" to the category, if needed', () => { + tracking.trackEvent({ category: 'a' }); + expect(calls).toEqual([ + ['send', { hitType: 'event', eventCategory: 'jaeger/a', eventAction: jasmine.any(String) }], + ]); + }); + + it('truncates values, if needed', () => { + const str = `jaeger/${getStr(600)}`; + tracking.trackEvent({ category: str, action: str, label: str }); + expect(calls).toEqual([ + [ + 'send', + { + hitType: 'event', + eventCategory: str.slice(0, 149), + eventAction: str.slice(0, 499), + eventLabel: str.slice(0, 499), + }, + ], + ]); + }); + }); + + it('converting raven-js errors', () => { + window.onunhandledrejection({ reason: new Error('abc') }); + expect(calls).toEqual([ + ['send', { hitType: 'exception', exDescription: jasmine.any(String), exFatal: false }], + ['send', { hitType: 'event', eventCategory: jasmine.any(String), eventAction: jasmine.any(String) }], + ]); + }); +}); From 22a7ac085b61db5a9a57f51cb91de8a0bb217e56 Mon Sep 17 00:00:00 2001 From: Joe Farro Date: Mon, 12 Feb 2018 00:55:23 -0500 Subject: [PATCH 09/18] Apply prettier to markdown files Signed-off-by: Joe Farro --- CHANGELOG.md | 129 ++++++++-------- CONTRIBUTING.md | 54 +++---- README.md | 24 +-- package.json | 10 +- src/index.js | 8 +- src/utils/DraggableManager/README.md | 211 +++++++++++++++------------ 6 files changed, 224 insertions(+), 212 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f78ba5902e..9006f54737 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,145 +1,136 @@ # Changes merged into master - ### [#169](https://github.com/jaegertracing/jaeger-ui/pull/169) Use Ant Design instead of Semantic UI -- Fix [#164](https://github.com/jaegertracing/jaeger-ui/issues/164) - Use Ant Design instead of Semantic UI -- Fix [#165](https://github.com/jaegertracing/jaeger-ui/issues/165) - Search results are shown without a date -- Fix [#69](https://github.com/jaegertracing/jaeger-ui/issues/69) - Missing endpoints in jaeger ui dropdown - +* Fix [#164](https://github.com/jaegertracing/jaeger-ui/issues/164) - Use Ant Design instead of Semantic UI +* Fix [#165](https://github.com/jaegertracing/jaeger-ui/issues/165) - Search results are shown without a date +* Fix [#69](https://github.com/jaegertracing/jaeger-ui/issues/69) - Missing endpoints in jaeger ui dropdown ### [#168](https://github.com/jaegertracing/jaeger-ui/pull/168) Fix 2 digit lookback (12h, 24h) parsing -- Fix [#167](https://github.com/jaegertracing/jaeger-ui/issues/167) - 12 and 24 hour search lookbacks not converted to start timestamp correctly - +* Fix [#167](https://github.com/jaegertracing/jaeger-ui/issues/167) - 12 and 24 hour search lookbacks not + converted to start timestamp correctly ### [#162](https://github.com/jaegertracing/jaeger-ui/pull/162) Only JSON.parse JSON strings in tags/logs values -- Fix [#146](https://github.com/jaegertracing/jaeger-ui/issues/146) - Tags with string type displayed as integers in UI, bigint js problem - +* Fix [#146](https://github.com/jaegertracing/jaeger-ui/issues/146) - Tags with string type displayed as + integers in UI, bigint js problem ### [#161](https://github.com/jaegertracing/jaeger-ui/pull/161) Add timezone tooltip to custom lookback form-field -- Fix [#154](https://github.com/jaegertracing/jaeger-ui/issues/154) - Explain time zone of the lookback parameter - +* Fix [#154](https://github.com/jaegertracing/jaeger-ui/issues/154) - Explain time zone of the lookback + parameter ### [#153](https://github.com/jaegertracing/jaeger-ui/pull/153) Add View Option for raw/unadjusted trace -- Fix [#152](https://github.com/jaegertracing/jaeger-ui/issues/152) - Add View Option for raw/unadjusted trace - +* Fix [#152](https://github.com/jaegertracing/jaeger-ui/issues/152) - Add View Option for raw/unadjusted trace ### [#147](https://github.com/jaegertracing/jaeger-ui/pull/147) Use logfmt for search tag input format -- Fix [#145](https://github.com/jaegertracing/jaeger-ui/issues/145) - Support logfmt for tags text input in the search form -- Fix [#11](https://github.com/jaegertracing/jaeger-ui/issues/11) - Document allowed operators on tag search - +* Fix [#145](https://github.com/jaegertracing/jaeger-ui/issues/145) - Support logfmt for tags text input in + the search form +* Fix [#11](https://github.com/jaegertracing/jaeger-ui/issues/11) - Document allowed operators on tag search ### [#143](https://github.com/jaegertracing/jaeger-ui/pull/143) Add a config value for the DAG cutoff -- Fix [#130](https://github.com/jaegertracing/jaeger-ui/issues/130) - Why maximum dependency length is set to 100 in DAG? - +* Fix [#130](https://github.com/jaegertracing/jaeger-ui/issues/130) - Why maximum dependency length is set to + 100 in DAG? ### [#141](https://github.com/jaegertracing/jaeger-ui/pull/141) `package.json#proxy` should proxy all `/api` requests -- Fix [#139](https://github.com/jaegertracing/jaeger-ui/issues/139) - Anyone konw how to open 16686 port? - +* Fix [#139](https://github.com/jaegertracing/jaeger-ui/issues/139) - Anyone konw how to open 16686 port? ### [#140](https://github.com/jaegertracing/jaeger-ui/pull/140) Encode service names in API calls -- Fix [#138](https://github.com/jaegertracing/jaeger-ui/issues/138) - Cannot find operations if there is '/' char in serviceName - +* Fix [#138](https://github.com/jaegertracing/jaeger-ui/issues/138) - Cannot find operations if there is '/' + char in serviceName ### [#136](https://github.com/jaegertracing/jaeger-ui/pull/136) Fix endless trace HTTP requests -- Fix [#128](https://github.com/jaegertracing/jaeger-ui/issues/128) - When trace id is invalid, Jaeger UI send this request forever - +* Fix [#128](https://github.com/jaegertracing/jaeger-ui/issues/128) - When trace id is invalid, Jaeger UI send + this request forever ### [#134](https://github.com/jaegertracing/jaeger-ui/pull/134) Fix trace name resolution -- Fix [#117](https://github.com/jaegertracing/jaeger-ui/issues/117) - traceName relies on traceID to equal spanID -- Fix [#129](https://github.com/jaegertracing/jaeger-ui/issues/129) - ¯\_(ツ)_/¯ is not very clear - +* Fix [#117](https://github.com/jaegertracing/jaeger-ui/issues/117) - traceName relies on traceID to equal + spanID +* Fix [#129](https://github.com/jaegertracing/jaeger-ui/issues/129) - ¯*( ツ )*/¯ is not very clear ### [#133](https://github.com/jaegertracing/jaeger-ui/pull/133) Better HTTP error messages - ### [#122](https://github.com/jaegertracing/jaeger-ui/pull/122) Make dependencies tab configurable - ### [#120](https://github.com/jaegertracing/jaeger-ui/pull/120) Add keyboard shortcut help modal - ### [#118](https://github.com/jaegertracing/jaeger-ui/pull/118) Handle `FOLLOWS_FROM` reference type -- Fix [#115](https://github.com/jaegertracing/jaeger-ui/issues/115) - Rendering traces with spans containing a 'FOLLOWS_FROM' reference seems broken - +* Fix [#115](https://github.com/jaegertracing/jaeger-ui/issues/115) - Rendering traces with spans containing a + 'FOLLOWS_FROM' reference seems broken ### [#110](https://github.com/jaegertracing/jaeger-ui/pull/110) Fix browser back button not working correctly -- Fix [#94](https://github.com/jaegertracing/jaeger-ui/issues/94) - Browser back button not working correctly - +* Fix [#94](https://github.com/jaegertracing/jaeger-ui/issues/94) - Browser back button not working correctly ### [#107](https://github.com/jaegertracing/jaeger-ui/pull/107) Embed UI config -The query service can embed custom UI configuration into `index.html`, speeding up the initial page load and allowing custom Google Analytics tracking IDs without requiring the UI bundle to be regenerated. This also lays the ground work for other UI configuration scenarios, in the future. - +The query service can embed custom UI configuration into `index.html`, speeding up the initial page load and +allowing custom Google Analytics tracking IDs without requiring the UI bundle to be regenerated. This also +lays the ground work for other UI configuration scenarios, in the future. ### [#97](https://github.com/jaegertracing/jaeger-ui/pull/97) Change to Apache license v.2 and add DCO / CONTRIBUTING.md - ### [#93](https://github.com/jaegertracing/jaeger-ui/pull/93) Keyboard shortcuts and minimap UX -- Fix [#89](https://github.com/uber/jaeger-ui/issues/89) - [trace view] Drag and release on timeline header row zooms into respective range -- Fix [#23](https://github.com/uber/jaeger-ui/issues/23) - [trace view] Navigate and zoom via minimap -- Fix [#22](https://github.com/uber/jaeger-ui/issues/22) - [trace view] Pan and zoom via keyboard shortcuts - +* Fix [#89](https://github.com/uber/jaeger-ui/issues/89) - [trace view] Drag and release on timeline header + row zooms into respective range +* Fix [#23](https://github.com/uber/jaeger-ui/issues/23) - [trace view] Navigate and zoom via minimap +* Fix [#22](https://github.com/uber/jaeger-ui/issues/22) - [trace view] Pan and zoom via keyboard shortcuts ### [#84](https://github.com/jaegertracing/jaeger-ui/pull/84) Improve search dropdowns -- Fix [#79](https://github.com/uber/jaeger-ui/issues/79) - Sort services and operations operations (case insensitive) -- Fix [#31](https://github.com/uber/jaeger-ui/issues/31) - Filter options based on contains instead of starts with -- Fix [#30](https://github.com/uber/jaeger-ui/issues/30) - Filter options based on case insensitive match - +* Fix [#79](https://github.com/uber/jaeger-ui/issues/79) - Sort services and operations operations (case + insensitive) +* Fix [#31](https://github.com/uber/jaeger-ui/issues/31) - Filter options based on contains instead of starts + with +* Fix [#30](https://github.com/uber/jaeger-ui/issues/30) - Filter options based on case insensitive match ### [#78](https://github.com/jaegertracing/jaeger-ui/pull/78) Custom menu via /api/config with project links as defaults -- Fix [#44](https://github.com/uber/jaeger-ui/issues/44) - Add configurable, persistent links to the header -- **Support for this is WIP in query service** - +* Fix [#44](https://github.com/uber/jaeger-ui/issues/44) - Add configurable, persistent links to the header +* **Support for this is WIP in query service** ### [#81](https://github.com/jaegertracing/jaeger-ui/pull/81) Fix Google Analytics tracking - ### [#77](https://github.com/jaegertracing/jaeger-ui/pull/77) Fix trace mini-map blurry when < 60 spans - ### [#74](https://github.com/jaegertracing/jaeger-ui/pull/74) Make left column adjustable in trace detail - ### [#71](https://github.com/jaegertracing/jaeger-ui/pull/71) [trave view] Mouseover expands truncated text to full length in left column - ### [#68](https://github.com/jaegertracing/jaeger-ui/pull/68) Virtualized scrolling for trace detail view -- Performance improved for initial loading, expanding span details, text search and scrolling - +* Performance improved for initial loading, expanding span details, text search and scrolling ### [#53](https://github.com/jaegertracing/jaeger-ui/pull/53) Refactor trace detail -- Partial fix for [#42](https://github.com/uber/jaeger-ui/issues/42) - Support URL prefix via homepage in package.json -- Scatterplot dots are sized based on number of spans -- Scatterplot dots mouseover shows trace name -- Clicking span detail left column collapses detail -- Clicking anywhere left of parent span name toggles children visibility -- Clip or hide span bars when zoomed in (instead of flush left) -- Label on span bars no longer off-screen -- Full width of the header is clickable for tags, process, and logs headers (instead of header text, only) -- Horizontal scrolling for wide content (e.g. long log values) (Fix [#58](https://github.com/uber/jaeger-ui/issues/58)) -- Tall content scrolls via entire table instead of single table cell -- Fix [#55](https://github.com/uber/jaeger-ui/issues/55) - Some tags were not being rendered due to clashing keys (observed in a log message) -- Fix [jaegertracing/jaeger#326](https://github.com/jaegertracing/jaeger/issues/326) - extraneous scrollbars in trace views -- Ticks in span graph made to match trace detail (in number and formatting) -- Fix [#49](https://github.com/uber/jaeger-ui/issues/42) - Span position in graph doesn't not match its position in the detail - +* Partial fix for [#42](https://github.com/uber/jaeger-ui/issues/42) - Support URL prefix via homepage in + package.json +* Scatterplot dots are sized based on number of spans +* Scatterplot dots mouseover shows trace name +* Clicking span detail left column collapses detail +* Clicking anywhere left of parent span name toggles children visibility +* Clip or hide span bars when zoomed in (instead of flush left) +* Label on span bars no longer off-screen +* Full width of the header is clickable for tags, process, and logs headers (instead of header text, only) +* Horizontal scrolling for wide content (e.g. long log values) (Fix + [#58](https://github.com/uber/jaeger-ui/issues/58)) +* Tall content scrolls via entire table instead of single table cell +* Fix [#55](https://github.com/uber/jaeger-ui/issues/55) - Some tags were not being rendered due to clashing + keys (observed in a log message) +* Fix [jaegertracing/jaeger#326](https://github.com/jaegertracing/jaeger/issues/326) - extraneous scrollbars + in trace views +* Ticks in span graph made to match trace detail (in number and formatting) +* Fix [#49](https://github.com/uber/jaeger-ui/issues/42) - Span position in graph doesn't not match its + position in the detail ### [Changes from before 2017-08-23 are not logged here](https://www.youtube.com/watch?v=NoAzpa1x7jU&feature=youtu.be&t=107) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d6e2ec0e9f..04c6905126 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,41 +2,37 @@ We'd love your help! -Jaeger is [Apache 2.0 licensed](LICENSE) and accepts contributions via GitHub -pull requests. This document outlines some of the conventions on development -workflow, commit message formatting, contact points and other resources to make -it easier to get your contribution accepted. +Jaeger is [Apache 2.0 licensed](LICENSE) and accepts contributions via GitHub pull requests. This document +outlines some of the conventions on development workflow, commit message formatting, contact points and other +resources to make it easier to get your contribution accepted. We gratefully welcome improvements to documentation as well as to code. # Certificate of Origin -By contributing to this project you agree to the [Developer Certificate of -Origin](https://developercertificate.org/) (DCO). This document was created -by the Linux Kernel community and is a simple statement that you, as a -contributor, have the legal right to make the contribution. See the [DCO](DCO) -file for details. +By contributing to this project you agree to the +[Developer Certificate of Origin](https://developercertificate.org/) (DCO). This document was created by the +Linux Kernel community and is a simple statement that you, as a contributor, have the legal right to make the +contribution. See the [DCO](DCO) file for details. ## Making A Change -*Before making any significant changes, please [open an -issue](https://github.com/jaegertracing/jaeger-ui/issues).* Discussing your proposed -changes ahead of time will make the contribution process smooth for everyone. +_Before making any significant changes, please +[open an issue](https://github.com/jaegertracing/jaeger-ui/issues)._ Discussing your proposed changes ahead of +time will make the contribution process smooth for everyone. -Once we've discussed your changes and you've got your code ready, make sure -that tests are passing and open your pull request. Your PR is most likely -to be accepted if it: +Once we've discussed your changes and you've got your code ready, make sure that tests are passing and open +your pull request. Your PR is most likely to be accepted if it: * Includes tests for new functionality. * References the original issue in description, e.g. "Resolves #123". -* Has a [good commit - message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). +* Has a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). * Each commit must be signed by the author ([see below](#sign-your-work)). ## License -By contributing your code, you agree to license your contribution under the terms -of the [Apache License](LICENSE). +By contributing your code, you agree to license your contribution under the terms of the +[Apache License](LICENSE). If you are adding a new file it should have a header like below. @@ -58,11 +54,9 @@ If you are adding a new file it should have a header like below. ## Sign your work -The sign-off is a simple line at the end of the explanation for the -patch, which certifies that you wrote it or otherwise have the right to -pass it on as an open-source patch. The rules are pretty simple: if you -can certify the below (from -[developercertificate.org](http://developercertificate.org/)): +The sign-off is a simple line at the end of the explanation for the patch, which certifies that you wrote it +or otherwise have the right to pass it on as an open-source patch. The rules are pretty simple: if you can +certify the below (from [developercertificate.org](http://developercertificate.org/)): ``` Developer Certificate of Origin @@ -122,12 +116,10 @@ git config --add alias.c "commit -s" Prefer to use [flow](https://flow.org/) for new code. -We use [`prettier`](https://prettier.io/), an "opinionated" code formatter. It -can be applied to both JavaScript and CSS source files via `yarn prettier`. +We use [`prettier`](https://prettier.io/), an "opinionated" code formatter. It can be applied to both +JavaScript and CSS source files via `yarn prettier`. -Then, most issues will be caught by the linter, which can be applied via `yarn -eslint`. +Then, most issues will be caught by the linter, which can be applied via `yarn eslint`. -Finally, we generally adhere to the -[Airbnb Style Guide](https://github.com/airbnb/javascript), with exceptions as -noted in our `.eslintrc`. +Finally, we generally adhere to the [Airbnb Style Guide](https://github.com/airbnb/javascript), with +exceptions as noted in our `.eslintrc`. diff --git a/README.md b/README.md index 64fbe822d8..e5b858d4a2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -[![ReadTheDocs][doc-img]][doc] [![Build Status][ci-img]][ci] [![Coverage Status][cov-img]][cov] [![FOSSA Status][fossa-img]][fossa] +[![ReadTheDocs][doc-img]][doc] [![Build Status][ci-img]][ci] [![Coverage Status][cov-img]][cov] +[![FOSSA Status][fossa-img]][fossa] # Jaeger UI @@ -35,8 +36,8 @@ npm install yarn ``` -Make sure you have the Jaeger Query service running on http://localhost:16686. -For example, you can run Jaeger all-in-one Docker image as descibed in the [documentation][aio-docs]. +Make sure you have the Jaeger Query service running on http://localhost:16686. For example, you can run Jaeger +all-in-one Docker image as descibed in the [documentation][aio-docs]. If you don't have it running locally, then tunnel to the correct host and port. @@ -52,13 +53,12 @@ npm start #### Commands -| Command | Description | -| ------- | ----------- | -| `npm start` | Starts development server with hot reloading and api proxy. | -| `npm test` | Runs all the tests | -| `npm run lint` | Lint the project (eslint, prettier, flow) | -| `npm run build` | Runs production build. Outputs files to `/dist`. | - +| Command | Description | +| --------------- | ----------------------------------------------------------- | +| `npm start` | Starts development server with hot reloading and api proxy. | +| `npm test` | Runs all the tests | +| `npm run lint` | Lint the project (eslint, prettier, flow) | +| `npm run build` | Runs production build. Outputs files to `/dist`. | ## Build @@ -69,10 +69,10 @@ npm install npm run build ``` - ## UI Configuration -See the [deployment guide](http://jaeger.readthedocs.io/en/latest/deployment/#ui-configuration) for details on configuring Google Analytics tracking and menu customizations. +See the [deployment guide](http://jaeger.readthedocs.io/en/latest/deployment/#ui-configuration) for details on +configuring Google Analytics tracking and menu customizations. [doc-img]: https://readthedocs.org/projects/jaeger/badge/?version=latest [doc]: http://jaeger.readthedocs.org/en/latest/ diff --git a/package.json b/package.json index ffa1c7d495..e9812369f9 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,8 @@ }, "scripts": { "start": "react-app-rewired start", - "start:ga-debug": "REACT_APP_GA_DEBUG=1 REACT_APP_VSN_STATE=$(./scripts/get-tracking-version.js) react-app-rewired start", + "start:ga-debug": + "REACT_APP_GA_DEBUG=1 REACT_APP_VSN_STATE=$(./scripts/get-tracking-version.js) react-app-rewired start", "build": "REACT_APP_VSN_STATE=$(./scripts/get-tracking-version.js) react-app-rewired build", "eject": "react-scripts eject", "test": "CI=1 react-app-rewired test --env=jsdom --color", @@ -99,8 +100,8 @@ "lint": "npm run eslint && npm run prettier && npm run flow && npm run check-license", "eslint": "eslint src", "check-license": "./scripts/check-license.sh", - "prettier": "prettier --write 'src/**/*.{js,json,css}'", - "flow": "flow; test $? -eq 0 -o $? -eq 2", + "prettier": "prettier --write 'src/**/*.{css,js,json,md}' '*.{css,js,json,md}'", + "flow": "glow", "precommit": "lint-staged" }, "jest": { @@ -117,6 +118,7 @@ "trailingComma": "es5" }, "lint-staged": { - "*.{css,js,json}": ["npm run prettier", "npm run check-license", "npm run test", "git add"] + "*.{css,js,json}": ["npm run lint", "npm run test", "git add"], + "*.md": ["npm run prettier", "git add"] } } diff --git a/src/index.js b/src/index.js index ede02d5c24..8a7fefe4ff 100644 --- a/src/index.js +++ b/src/index.js @@ -12,15 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -/* eslint-disable import/first */ - import React from 'react'; import ReactDOM from 'react-dom'; import { document } from 'global'; -import JaegerUIApp from './components/App'; -import { context as trackingContext } from './utils/tracking'; - import 'u-basscss/css/flexbox.css'; import 'u-basscss/css/layout.css'; import 'u-basscss/css/margin.css'; @@ -28,6 +23,9 @@ import 'u-basscss/css/padding.css'; import 'u-basscss/css/position.css'; import 'u-basscss/css/typography.css'; +import JaegerUIApp from './components/App'; +import { context as trackingContext } from './utils/tracking'; + const UI_ROOT_ID = 'jaeger-ui-root'; /* istanbul ignore if */ diff --git a/src/utils/DraggableManager/README.md b/src/utils/DraggableManager/README.md index b054c317c6..f2754e091b 100644 --- a/src/utils/DraggableManager/README.md +++ b/src/utils/DraggableManager/README.md @@ -1,49 +1,54 @@ # DraggbleManager Information and Demo -In the `src/utils/DraggableManager/demo` folder there is a small project that demonstrates the use of the `DraggableManager` utility. +In the `src/utils/DraggableManager/demo` folder there is a small project that demonstrates the use of the +`DraggableManager` utility. The demo contains two components: -- `DividerDemo`, which occupies the top half of the web page -- `RegionDemo`, which occupies the bottom half of the web page, as shown in the GIF, below +* `DividerDemo`, which occupies the top half of the web page +* `RegionDemo`, which occupies the bottom half of the web page, as shown in the GIF, below ![GIF of Demo](demo/demo-ux.gif) - ## Caveat -This DraggableManager utility does not actually "drag" anything, it does not move or drag DOM elements, it just tells us where the mouse is while the mouse is down. Primarily, it listens for `mousedown` and subsequent `mousemove` and then finally `mouseup` events. (It listens to `window` for the `mousemove` and `mouseup` events.) - -What we do with that information is up to us. This is mentioned because you need to handle the DraggableManager callbacks *to create the illusion of dragging*. +This DraggableManager utility does not actually "drag" anything, it does not move or drag DOM elements, it +just tells us where the mouse is while the mouse is down. Primarily, it listens for `mousedown` and subsequent +`mousemove` and then finally `mouseup` events. (It listens to `window` for the `mousemove` and `mouseup` +events.) +What we do with that information is up to us. This is mentioned because you need to handle the +DraggableManager callbacks _to create the illusion of dragging_. ## In brief DraggableManager instances provide three (and a half) conveniences: -- Handle mouse events related to dragging. -- Maps `MouseEvent.clientX` from the [client area](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/clientX) to the local context (yielding `x` (pixels) and `value` (0 -> 1, e.g, `x/width`)). -- Maintains a sense of state in terms of whether or not the subject DOM element is being dragged. For example, it fires `onMouseMove` callbacks when not being dragged and `onDragMove` when being dragged. -- Two other minor conveniences (relating to window events) +* Handle mouse events related to dragging. +* Maps `MouseEvent.clientX` from the + [client area](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/clientX) to the local context + (yielding `x` (pixels) and `value` (0 -> 1, e.g, `x/width`)). +* Maintains a sense of state in terms of whether or not the subject DOM element is being dragged. For example, + it fires `onMouseMove` callbacks when not being dragged and `onDragMove` when being dragged. +* Two other minor conveniences (relating to window events) And, DraggableManager instances have two (or three) primary requirements: -- Mouse events need to be piped into it -- The `getBounds()` constructor parameter must be provided -- At least some of the callbacks need to be handled - +* Mouse events need to be piped into it +* The `getBounds()` constructor parameter must be provided +* At least some of the callbacks need to be handled ## Conveniences - ### Handles the mouse events related to dragging -For the purposes of handling mouse events related to the intended dragging functionality, DraggableManager instances expose the following methods (among others): +For the purposes of handling mouse events related to the intended dragging functionality, DraggableManager +instances expose the following methods (among others): -- `handleMouseEnter` -- `handleMouseMove` -- `handleMouseLeave` -- `handleMouseDown` +* `handleMouseEnter` +* `handleMouseMove` +* `handleMouseLeave` +* `handleMouseDown` To use a DraggableManager instance, relevant mouse events should be piped to the above handlers: @@ -55,11 +60,13 @@ To use a DraggableManager instance, relevant mouse events should be piped to the Note: Not all handlers are always necessary. See "Mouse events need to be piped into it" for more details. - ### Maps the `clientX` to `x` and `value` -`MouseEvent` (and `SyntheticMouseEvent`) events provide the [`clientX`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/clientX) property, which generally needs some adjustments before it's useful. For instance, in the following snippet we transform `clientX` to the `x` within the `
`. The `value` is simply the `x/width` ratio, which is pretty much the percent but divided by `100`. - +`MouseEvent` (and `SyntheticMouseEvent`) events provide the +[`clientX`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/clientX) property, which generally +needs some adjustments before it's useful. For instance, in the following snippet we transform `clientX` to +the `x` within the `
`. The `value` is simply the `x/width` ratio, which is pretty much the percent but +divided by `100`. ```jsx
@@ -74,56 +81,61 @@ Note: Not all handlers are always necessary. See "Mouse events need to be piped console.log('position along the width: ', localX / width); }} /> -
+
; ``` -In other words, DraggableManager instances convert the data to the relevant context. (The "relevant context" is, naturally, varies... see the `getBounds()` constructor parameter below). - +In other words, DraggableManager instances convert the data to the relevant context. (The "relevant context" +is, naturally, varies... see the `getBounds()` constructor parameter below). ### Maintains a sense of state The callbacks for DraggableManager instances are: -- onMouseEnter -- onMouseLeave -- onMouseMove -- onDragStart -- onDragMove -- onDragEnd - -Implicit in the breakdown of the callbacks is the notion that `onDrag*` callbacks are fired when dragging and `onMouse*` callbacks are issued, otherwise. +* onMouseEnter +* onMouseLeave +* onMouseMove +* onDragStart +* onDragMove +* onDragEnd -Therefore, using the DraggableManager util relieves us of the necessity of keeping track of whether we are currently dragging or not. +Implicit in the breakdown of the callbacks is the notion that `onDrag*` callbacks are fired when dragging and +`onMouse*` callbacks are issued, otherwise. +Therefore, using the DraggableManager util relieves us of the necessity of keeping track of whether we are +currently dragging or not. ### Two other minor conveniences -When dragging starts, the util then switches over to listening to window events (`mousemove` and `mouseup`). This prevents the dragging from having strange behavior if / when the user moves the mouse anywhere on the page. +When dragging starts, the util then switches over to listening to window events (`mousemove` and `mouseup`). +This prevents the dragging from having strange behavior if / when the user moves the mouse anywhere on the +page. Last but not least... -The util listens for window resize events and makes adjustments accordingly, preventing things from going crazy (due to miscalibration) if the user resizes the window. This primary relates to the `getBounds()` constructor option (see below). - - +The util listens for window resize events and makes adjustments accordingly, preventing things from going +crazy (due to miscalibration) if the user resizes the window. This primary relates to the `getBounds()` +constructor option (see below). ## Requirements ### Mouse events need to be piped into it -In my use, DraggbaleManager instances become the receiver of the relevant mouse events instead of handlers on the React component. +In my use, DraggbaleManager instances become the receiver of the relevant mouse events instead of handlers on +the React component. -For instance, if implementing a draggable divider (see `DividerDemo.js` and the top half of the gif), only `onMouseDown` needs to be handled: +For instance, if implementing a draggable divider (see `DividerDemo.js` and the top half of the gif), only +`onMouseDown` needs to be handled: ```jsx
-
-
+
+
; ``` -But, if implementing the ability to drag a sub-range (see `RegionDemo.js` and the bottom of demo gif), you generally want to show a vertical line at the mouse cursor until the dragging starts (`onMouseDown`), then you want to draw the region being dragged. So, the `onMouseMove`, `onMouseLeave` and `onMouseDown` handlers are necessary: +But, if implementing the ability to drag a sub-range (see `RegionDemo.js` and the bottom of demo gif), you +generally want to show a vertical line at the mouse cursor until the dragging starts (`onMouseDown`), then you +want to draw the region being dragged. So, the `onMouseMove`, `onMouseLeave` and `onMouseDown` handlers are +necessary: ```jsx
{/* Draw visuals for the currently dragged range, otherwise empty */} -
+
; ``` ### `getBounds()` constructor parameter @@ -151,7 +163,9 @@ type DraggableBounds = { }; ``` -This generally amounts to calling [`Element#getBoundingClientRect()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect) on the DOM element that defines the valid dragging range. +This generally amounts to calling +[`Element#getBoundingClientRect()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect) +on the DOM element that defines the valid dragging range. For instance, in the `DividerDemo`, the function used is `DivideDemo#_getDraggingBounds()`: @@ -172,23 +186,31 @@ _getDraggingBounds = (): DraggableBounds => { In the snippet above, `this._realmElm` is the `
` that fills the green draggable region. -On the other hand, if you need more flexibility, this function can ignore the DOM altogether and do something else entirely. It just needs to return an object with `clientXLeft` and `width` properties, at the minimum. - -`maxValue` and `minValue` are optional and will restrict the extent of the dragging. They are in terms of `value`, not `x`. +On the other hand, if you need more flexibility, this function can ignore the DOM altogether and do something +else entirely. It just needs to return an object with `clientXLeft` and `width` properties, at the minimum. +`maxValue` and `minValue` are optional and will restrict the extent of the dragging. They are in terms of +`value`, not `x`. ### The callbacks need to be handled Last but not least, if the callbacks are ignored, nothing happens. -In the `DividerDemo`, we're only interested in repositioning the divider when it is dragged. We don't care about mouse related callbacks. So, only the drag related callbacks are handled. And, all of the drag callbacks are handled in the same way: we update the position of the divider. Done. See `DividerDemo#_handleDragEvent()`. +In the `DividerDemo`, we're only interested in repositioning the divider when it is dragged. We don't care +about mouse related callbacks. So, only the drag related callbacks are handled. And, all of the drag callbacks +are handled in the same way: we update the position of the divider. Done. See +`DividerDemo#_handleDragEvent()`. -In the other scenario, `RegionDemo`, we care about showing the red vertical line for mouse-over. This sort of indicates to the user they can click and drag, and when they drag we want to show a region that spans the current drag. So, we handle the mousemove and mouseleave callbacks along with the drag callbacks. +In the other scenario, `RegionDemo`, we care about showing the red vertical line for mouse-over. This sort of +indicates to the user they can click and drag, and when they drag we want to show a region that spans the +current drag. So, we handle the mousemove and mouseleave callbacks along with the drag callbacks. -The `RegionDemo` is a bit more involved, so, to break down how we handle the callbacks... First, we store the following state (in the parent element, incidentally): +The `RegionDemo` is a bit more involved, so, to break down how we handle the callbacks... First, we store the +following state (in the parent element, incidentally): -- `regionCursor` is where we draw the cursor indicator (a red vertical line, in the demo). -- `regionDragging` represents the start (at index `0`) and current position (at index `1`) of the region currently being dragged. +* `regionCursor` is where we draw the cursor indicator (a red vertical line, in the demo). +* `regionDragging` represents the start (at index `0`) and current position (at index `1`) of the region + currently being dragged. ``` { @@ -199,28 +221,29 @@ The `RegionDemo` is a bit more involved, so, to break down how we handle the cal Then, we handle the callbacks as follows: -- `onMouseMove` - - Set `regionCursor` to `value` - - This allows us to draw the red vertical line at the cursor -- `onMouseLeave` - - Set `regionCursor` to `null` - - So we know not to draw the red vertical line -- `onDragStart` - - Set `regionDragging` to `[value, value]` - - This allows us to draw the dragging region -- `onDragMove` - - Set `regionDragging` to `[regionDragging[0], value]` - - Again, for drawing the dragging region. We keep `regionDragging[0]` as-is so we always know where the drag started -- `onDragEnd` - - Set `regionDragging` to `null`, set `regionCursor` to `value` - - Setting `regionDragging` to `null` lets us know not to draw the region, and setting `regionCursor` lets us know to draw the cursor right where the user left off - -This is a contrived demo, so `onDragEnd` is kind of boring... Usually we would do something more interesting with the final `x` or `value`. - +* `onMouseMove` + * Set `regionCursor` to `value` + * This allows us to draw the red vertical line at the cursor +* `onMouseLeave` + * Set `regionCursor` to `null` + * So we know not to draw the red vertical line +* `onDragStart` + * Set `regionDragging` to `[value, value]` + * This allows us to draw the dragging region +* `onDragMove` + * Set `regionDragging` to `[regionDragging[0], value]` + * Again, for drawing the dragging region. We keep `regionDragging[0]` as-is so we always know where the drag + started +* `onDragEnd` + * Set `regionDragging` to `null`, set `regionCursor` to `value` + * Setting `regionDragging` to `null` lets us know not to draw the region, and setting `regionCursor` lets us + know to draw the cursor right where the user left off + +This is a contrived demo, so `onDragEnd` is kind of boring... Usually we would do something more interesting +with the final `x` or `value`. ## API - ### Constants `updateTypes` Used as the `type` field on `DraggingUpdate` objects. @@ -236,7 +259,6 @@ Used as the `type` field on `DraggingUpdate` objects. }; ``` - ### Type `DraggingUpdate` The data type issued for all callbacks. @@ -252,7 +274,6 @@ type DraggingUpdate = { }; ``` - ### Type `DraggableBounds` The type the `getBounds()` constructor parameter must return. @@ -266,12 +287,16 @@ type DraggableBounds = { }; ``` -`clientXLeft` is used to convert [`MouseEvent.clientX`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/clientX) from the client area to the dragging area. - -`maxValue` and `minValue` are in terms of `value` on the updates, e.g. they are in the range from `[0, 1]` where `0` is the far left (e.g. style `left: 0;`) end of the draggable region and `1` is the far right end (style `right: 0`). If set, they will restrict the `x` and `value` issued by the callbacks. +`clientXLeft` is used to convert +[`MouseEvent.clientX`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/clientX) from the client +area to the dragging area. -`width` is used to convert `x` to `value` and is also the span on which `minValue` and `maxValue` are mapped onto when calculating `x` and `value` for issuing callbacks. +`maxValue` and `minValue` are in terms of `value` on the updates, e.g. they are in the range from `[0, 1]` +where `0` is the far left (e.g. style `left: 0;`) end of the draggable region and `1` is the far right end +(style `right: 0`). If set, they will restrict the `x` and `value` issued by the callbacks. +`width` is used to convert `x` to `value` and is also the span on which `minValue` and `maxValue` are mapped +onto when calculating `x` and `value` for issuing callbacks. ### Constructor parameters @@ -289,20 +314,24 @@ type DraggableManagerOptions = { }; ``` -`getBounds()` is used to map the `clientX` to whatever the dragging context is. **It is called lazily** and the returned value is cached, until either `DraggableManager#resetBounds()` is called, the window is resized (when `resetBoundsOnResize` is `true`) or `DraggableManager#dispose()` is called. +`getBounds()` is used to map the `clientX` to whatever the dragging context is. **It is called lazily** and +the returned value is cached, until either `DraggableManager#resetBounds()` is called, the window is resized +(when `resetBoundsOnResize` is `true`) or `DraggableManager#dispose()` is called. -The callbacks are all optional. The callbacks all present the same data (`DraggingUpdate`), with the `type` field being set based on which callback is firing (e.g. `type` is `'MOUSE_ENTER'` when `onMouseEnter` is fired), and the `x` and `value` representing the last know position of the mouse cursor. +The callbacks are all optional. The callbacks all present the same data (`DraggingUpdate`), with the `type` +field being set based on which callback is firing (e.g. `type` is `'MOUSE_ENTER'` when `onMouseEnter` is +fired), and the `x` and `value` representing the last know position of the mouse cursor. -If `resetBoundsOnResize` is `true`, the instance resets the cached `DraggableBounds` when the window is resized. - -`tag` is an optional string parameter. It is a convenience field for distinguishing different `DraggableManager` instances. If set on the constructor, it is set on every `DraggingUpdate` that is issued. +If `resetBoundsOnResize` is `true`, the instance resets the cached `DraggableBounds` when the window is +resized. +`tag` is an optional string parameter. It is a convenience field for distinguishing different +`DraggableManager` instances. If set on the constructor, it is set on every `DraggingUpdate` that is issued. ### `DraggableManager# isDragging()` -Returns `true` when the instance is in a dragged state, e.g. after `onDragStart` is fired and before `onDragEnd` is fired. - - +Returns `true` when the instance is in a dragged state, e.g. after `onDragStart` is fired and before +`onDragEnd` is fired. ### `DraggableManager# dispose()` From 263c8aebeaa782178499a4e44ba054fc0641c295 Mon Sep 17 00:00:00 2001 From: Joe Farro Date: Mon, 12 Feb 2018 06:46:57 -0500 Subject: [PATCH 10/18] Error tracking fn name fallback, CSS import order Signed-off-by: Joe Farro --- src/index.js | 8 +++++--- src/utils/tracking/conv-raven-to-ga.js | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/index.js b/src/index.js index 8a7fefe4ff..39f0319675 100644 --- a/src/index.js +++ b/src/index.js @@ -16,6 +16,11 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { document } from 'global'; +import JaegerUIApp from './components/App'; +import { context as trackingContext } from './utils/tracking'; + +/* eslint-disable import/first */ +// these need to go after the App import import 'u-basscss/css/flexbox.css'; import 'u-basscss/css/layout.css'; import 'u-basscss/css/margin.css'; @@ -23,9 +28,6 @@ import 'u-basscss/css/padding.css'; import 'u-basscss/css/position.css'; import 'u-basscss/css/typography.css'; -import JaegerUIApp from './components/App'; -import { context as trackingContext } from './utils/tracking'; - const UI_ROOT_ID = 'jaeger-ui-root'; /* istanbul ignore if */ diff --git a/src/utils/tracking/conv-raven-to-ga.js b/src/utils/tracking/conv-raven-to-ga.js index e179ff65e3..eaa882965b 100644 --- a/src/utils/tracking/conv-raven-to-ga.js +++ b/src/utils/tracking/conv-raven-to-ga.js @@ -127,7 +127,7 @@ function convException(errValue) { const message = convErrorMessage(`${errValue.type}: ${errValue.value}`, 149); const frames = errValue.stacktrace.frames.map(fr => { const filename = fr.filename.replace(origin, '').replace(/^\/static\/js\//i, ''); - const fn = collapseWhitespace(fr.function); + const fn = collapseWhitespace(fr.function || '??'); return { filename, fn }; }); const joiner = []; @@ -339,7 +339,7 @@ export default function convRavenToGa({ data }: RavenTransportOptions) { const url = truncate(data.request.url.replace(origin, ''), 50); const { word: page } = getSym(NAV_SYMBOLS, url); const value = Math.round(data.extra['session:duration'] / 1000); - const category = `jaeger/${page}/error`; + const category = `jaeger/error/${page}`; let action = [message, data.tags.git, url, '', stack].filter(v => v != null).join('\n'); action = truncate(action, 499); const label = getLabel(message, page, value, data.tags.git, data.breadcrumbs.values); From dfd9abbada5723c10810ac43ddf3afe7f1beee1a Mon Sep 17 00:00:00 2001 From: Joe Farro Date: Thu, 1 Mar 2018 00:04:28 -0500 Subject: [PATCH 11/18] GA event tracking in trace view, tests are TODO Signed-off-by: Joe Farro --- .../TracePage/KeyboardShortcutsHelp.css | 1 - .../TracePage/KeyboardShortcutsHelp.js | 4 +- .../TracePage/KeyboardShortcutsHelp.track.js | 26 +++++++ .../TracePage/SpanGraph/ViewingLayer.js | 6 +- src/components/TracePage/TracePageHeader.js | 15 +++- .../TracePage/TracePageHeader.track.js | 34 ++++++++ .../TraceTimelineViewer/SpanBarRow.track.js | 42 ++++++++++ .../SpanDetail/AccordianKeyValues.js | 3 +- .../TraceTimelineViewer/SpanDetail/index.js | 2 + .../SpanDetail/index.track.js | 70 +++++++++++++++++ .../SpanDetailRow.track.js | 32 ++++++++ .../TimelineHeaderRow/TimelineHeaderRow.js | 2 +- .../TimelineHeaderRow.track.js | 33 ++++++++ .../TimelineHeaderRow/TimelineViewingLayer.js | 4 +- .../TracePage/TraceTimelineViewer/duck.js | 2 +- .../TracePage/TraceTimelineViewer/index.js | 2 +- src/components/TracePage/index.js | 19 +++-- src/components/TracePage/index.track.js | 77 +++++++++++++++++++ src/middlewares/index.js | 2 + src/middlewares/track.js | 47 +++++++++++ src/types/index.js | 9 +-- src/utils/configure-store.js | 5 +- src/utils/tracking/index.js | 2 +- 23 files changed, 413 insertions(+), 26 deletions(-) create mode 100644 src/components/TracePage/KeyboardShortcutsHelp.track.js create mode 100644 src/components/TracePage/TracePageHeader.track.js create mode 100644 src/components/TracePage/TraceTimelineViewer/SpanBarRow.track.js create mode 100644 src/components/TracePage/TraceTimelineViewer/SpanDetail/index.track.js create mode 100644 src/components/TracePage/TraceTimelineViewer/SpanDetailRow.track.js create mode 100644 src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineHeaderRow.track.js create mode 100644 src/components/TracePage/index.track.js create mode 100644 src/middlewares/track.js diff --git a/src/components/TracePage/KeyboardShortcutsHelp.css b/src/components/TracePage/KeyboardShortcutsHelp.css index b7c0990072..1cafbe6a76 100644 --- a/src/components/TracePage/KeyboardShortcutsHelp.css +++ b/src/components/TracePage/KeyboardShortcutsHelp.css @@ -24,7 +24,6 @@ limitations under the License. border: 1px solid #e8e8e8; border-bottom: 1px solid #ddd; color: #000; - margin-right: 0.4em; font-family: monospace; padding: 0.25em 0.3em; } diff --git a/src/components/TracePage/KeyboardShortcutsHelp.js b/src/components/TracePage/KeyboardShortcutsHelp.js index bf6b9cc96d..f2037ecf61 100644 --- a/src/components/TracePage/KeyboardShortcutsHelp.js +++ b/src/components/TracePage/KeyboardShortcutsHelp.js @@ -18,6 +18,7 @@ import React from 'react'; import { Button, Modal, Table } from 'antd'; import { kbdMappings } from './keyboard-shortcuts'; +import track from './KeyboardShortcutsHelp.track'; import './KeyboardShortcutsHelp.css'; @@ -56,13 +57,14 @@ function convertKeys(keyConfig: string | string[]): string[][] { } function helpModal() { + track(); const data = []; Object.keys(kbdMappings).forEach(title => { const keyConfigs = convertKeys(kbdMappings[title]); data.push( ...keyConfigs.map(config => ({ key: String(config), - kbds: config.map(s => {s}), + kbds: {config.join(' ')}, description: descriptions[title], })) ); diff --git a/src/components/TracePage/KeyboardShortcutsHelp.track.js b/src/components/TracePage/KeyboardShortcutsHelp.track.js new file mode 100644 index 0000000000..40236e1628 --- /dev/null +++ b/src/components/TracePage/KeyboardShortcutsHelp.track.js @@ -0,0 +1,26 @@ +// @flow + +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { trackEvent } from '../../utils/tracking'; + +const context = 'jaeger/ux/trace/kbd-modal'; + +export default function trackKbdHelpModal() { + trackEvent({ + category: context, + action: 'open', + }); +} diff --git a/src/components/TracePage/SpanGraph/ViewingLayer.js b/src/components/TracePage/SpanGraph/ViewingLayer.js index 174fc6d889..6c2a063b0a 100644 --- a/src/components/TracePage/SpanGraph/ViewingLayer.js +++ b/src/components/TracePage/SpanGraph/ViewingLayer.js @@ -28,7 +28,7 @@ import './ViewingLayer.css'; type ViewingLayerProps = { height: number, numTicks: number, - updateViewRangeTime: (number, number) => void, + updateViewRangeTime: (number, number, ?string) => void, updateNextViewRangeTime: ViewRangeTimeUpdate => void, viewRange: ViewRange, }; @@ -188,7 +188,7 @@ export default class ViewingLayer extends React.PureComponent { @@ -220,7 +220,7 @@ export default class ViewingLayer extends React.PureComponent - + Trace JSON - + Trace JSON (unadjusted) diff --git a/src/components/TracePage/TracePageHeader.track.js b/src/components/TracePage/TracePageHeader.track.js new file mode 100644 index 0000000000..68e50e50a9 --- /dev/null +++ b/src/components/TracePage/TracePageHeader.track.js @@ -0,0 +1,34 @@ +// @flow + +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { trackEvent } from '../../utils/tracking'; + +const altViewCtx = 'jaeger/ux/trace/alt-view'; +const slimHeaderCtx = 'jaeger/ux/trace/slim-header'; + +export function trackAltView() { + trackEvent({ + category: altViewCtx, + action: 'open', + }); +} + +export function trackSlimHeader(isOpen: boolean) { + trackEvent({ + category: slimHeaderCtx, + action: isOpen ? 'open' : 'close', + }); +} diff --git a/src/components/TracePage/TraceTimelineViewer/SpanBarRow.track.js b/src/components/TracePage/TraceTimelineViewer/SpanBarRow.track.js new file mode 100644 index 0000000000..ec97e68c00 --- /dev/null +++ b/src/components/TracePage/TraceTimelineViewer/SpanBarRow.track.js @@ -0,0 +1,42 @@ +// @flow + +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import type { Store } from 'redux'; + +import { actionTypes as types } from './duck'; +import { trackEvent } from '../../../utils/tracking'; + +const context = 'jaeger/ux/trace/timeline/parent'; + +function trackParent(store: Store, action: any) { + const st = store.getState(); + const { spanID } = action.payload; + const traceID = st.traceTimeline.traceID; + const isHidden = st.traceTimeline.childrenHiddenIDs.has(spanID); + const span = st.trace.traces[traceID].spans.find(sp => sp.spanID === spanID); + if (span) { + trackEvent({ + category: context, + action: isHidden ? 'open' : 'close', + value: span.depth, + }); + } +} + +// eslint-disable-next-line import/prefer-default-export +export const middlewareHooks = { + [types.CHILDREN_TOGGLE]: trackParent, +}; diff --git a/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianKeyValues.js b/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianKeyValues.js index 946b8df61e..d0eb11d797 100644 --- a/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianKeyValues.js +++ b/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianKeyValues.js @@ -25,7 +25,7 @@ import KeyValuesTable from './KeyValuesTable'; import './AccordianKeyValues.css'; type AccordianKeyValuesProps = { - className: ?string, + className?: ?string, data: { key: string, value: any }[], highContrast?: boolean, isOpen: boolean, @@ -86,5 +86,6 @@ export default function AccordianKeyValues(props: AccordianKeyValuesProps) { } AccordianKeyValues.defaultProps = { + className: null, highContrast: false, }; diff --git a/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.js b/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.js index f0df932f28..062ed8a712 100644 --- a/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.js +++ b/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.js @@ -1,3 +1,5 @@ +// @flow + // Copyright (c) 2017 Uber Technologies, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.track.js b/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.track.js new file mode 100644 index 0000000000..f2ce2662e3 --- /dev/null +++ b/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.track.js @@ -0,0 +1,70 @@ +// @flow + +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { actionTypes as types } from '../duck'; +import { trackEvent } from '../../../../utils/tracking'; + +const baseContext = 'jaeger/ux/trace/timeline'; + +const tagsContext = `${baseContext}/tags`; +const processContext = `${baseContext}/process`; +const logsContext = `${baseContext}/logs`; +const logsItemContext = `${baseContext}/logs/item`; + +function getCmd(isOpen: boolean) { + return isOpen ? 'open' : 'close'; +} + +export function logs(isOpen: boolean) { + trackEvent({ + category: logsContext, + action: getCmd(isOpen), + }); +} + +export function logsItem(isOpen: boolean) { + trackEvent({ + category: logsItemContext, + action: getCmd(isOpen), + }); +} + +export function process(isOpen: boolean) { + trackEvent({ + category: processContext, + action: getCmd(isOpen), + }); +} + +export function tags(isOpen: boolean) { + trackEvent({ + category: tagsContext, + action: getCmd(isOpen), + }); +} + +const getDetail = (store, action) => store.getState().traceTimeline.detailStates.get(action.payload.spanID); + +export const middlewareHooks = { + [types.DETAIL_TAGS_TOGGLE]: (store, action) => tags(!getDetail(store, action).isTagsOpen), + [types.DETAIL_PROCESS_TOGGLE]: (store, action) => process(!getDetail(store, action).isProcessOpen), + [types.DETAIL_LOGS_TOGGLE]: (store, action) => logs(!getDetail(store, action).logs.isOpen), + [types.DETAIL_LOG_ITEM_TOGGLE]: (store, action) => { + const detail = getDetail(store, action); + const { logItem } = action.payload; + logsItem(!detail.logs.openedItems.has(logItem)); + }, +}; diff --git a/src/components/TracePage/TraceTimelineViewer/SpanDetailRow.track.js b/src/components/TracePage/TraceTimelineViewer/SpanDetailRow.track.js new file mode 100644 index 0000000000..71623c1375 --- /dev/null +++ b/src/components/TracePage/TraceTimelineViewer/SpanDetailRow.track.js @@ -0,0 +1,32 @@ +// @flow + +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { actionTypes as types } from './duck'; +import { trackEvent } from '../../../utils/tracking'; + +const context = 'jaeger/ux/trace/timeline/row'; + +export default function trackRow(isOpen: boolean) { + trackEvent({ + category: context, + action: isOpen ? 'open' : 'close', + }); +} + +export const middlewareHooks = { + [types.DETAIL_TOGGLE]: (store, action) => + trackRow(!store.getState().traceTimeline.detailStates.has(action.payload.spanID)), +}; diff --git a/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineHeaderRow.js b/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineHeaderRow.js index 333a61390a..ba3fc2838a 100644 --- a/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineHeaderRow.js +++ b/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineHeaderRow.js @@ -30,7 +30,7 @@ type TimelineHeaderRowProps = { numTicks: number, onColummWidthChange: number => void, updateNextViewRangeTime: ViewRangeTimeUpdate => void, - updateViewRangeTime: (number, number) => void, + updateViewRangeTime: (number, number, ?string) => void, viewRangeTime: ViewRangeTime, }; diff --git a/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineHeaderRow.track.js b/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineHeaderRow.track.js new file mode 100644 index 0000000000..81071facf4 --- /dev/null +++ b/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineHeaderRow.track.js @@ -0,0 +1,33 @@ +// @flow + +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { actionTypes as types } from '../duck'; +import { trackEvent } from '../../../../utils/tracking'; + +const cmd = 'resize'; +const context = 'jaeger/ux/trace/timeline/column'; + +export default function track(value: number) { + trackEvent({ + category: context, + action: cmd, + value: Math.round(value * 1000), + }); +} + +export const middlewareHooks = { + [types.SET_SPAN_NAME_COLUMN_WIDTH]: (_, action) => track(action.payload.width), +}; diff --git a/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineViewingLayer.js b/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineViewingLayer.js index 16d52f3de5..42bfd34770 100644 --- a/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineViewingLayer.js +++ b/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineViewingLayer.js @@ -31,7 +31,7 @@ type TimelineViewingLayerProps = { */ boundsInvalidator: ?any, updateNextViewRangeTime: ViewRangeTimeUpdate => void, - updateViewRangeTime: (number, number) => void, + updateViewRangeTime: (number, number, ?string) => void, viewRangeTime: ViewRangeTime, }; @@ -179,7 +179,7 @@ export default class TimelineViewingLayer extends React.PureComponent void, - updateViewRangeTime: (number, number) => void, + updateViewRangeTime: (number, number, ?string) => void, viewRange: ViewRange, }; diff --git a/src/components/TracePage/index.js b/src/components/TracePage/index.js index f218fa9ae5..de5fb95e9e 100644 --- a/src/components/TracePage/index.js +++ b/src/components/TracePage/index.js @@ -23,12 +23,14 @@ import { connect } from 'react-redux'; import type { RouterHistory, Match } from 'react-router-dom'; import { bindActionCreators } from 'redux'; +import * as track from './index.track'; import type { CombokeysHandler, ShortcutCallbacks } from './keyboard-shortcuts'; import { init as initShortcuts, reset as resetShortcuts } from './keyboard-shortcuts'; import { cancel as cancelScroll, scrollBy, scrollTo } from './scroll-page'; import ScrollManager from './ScrollManager'; import SpanGraph from './SpanGraph'; import TracePageHeader from './TracePageHeader'; +import { trackSlimHeader } from './TracePageHeader.track'; import TraceTimelineViewer from './TraceTimelineViewer'; import type { ViewRange, ViewRangeTimeUpdate } from './types'; import ErrorMessage from '../common/ErrorMessage'; @@ -122,7 +124,7 @@ export default class TracePage extends React.PureComponent this._adjustViewRange(a, b); + const adjViewRange = (a: number, b: number) => this._adjustViewRange(a, b, 'kbd'); const shortcutCallbacks = makeShortcutCallbacks(adjViewRange); shortcutCallbacks.scrollPageDown = scrollPageDown; shortcutCallbacks.scrollPageUp = scrollPageUp; @@ -161,7 +163,7 @@ export default class TracePage extends React.PureComponent { @@ -191,10 +193,15 @@ export default class TracePage extends React.PureComponent { + track.trackFilter(textFilter); this.setState({ textFilter }); }; - updateViewRangeTime = (start: number, end: number) => { + updateViewRangeTime = (start: number, end: number, trackSrc?: string) => { + if (trackSrc) { + const trackCmd = track.getRangeCmd([start, end], this.state.viewRange.time.current); + track.trackRange(trackCmd, trackSrc); + } const time = { current: [start, end] }; const viewRange = { ...this.state.viewRange, time }; this.setState({ viewRange }); @@ -207,7 +214,9 @@ export default class TracePage extends React.PureComponent { - this.setState({ slimView: !this.state.slimView }); + const { slimView } = this.state; + trackSlimHeader(slimView); + this.setState({ slimView: !slimView }); }; ensureTraceFetched() { diff --git a/src/components/TracePage/index.track.js b/src/components/TracePage/index.track.js new file mode 100644 index 0000000000..0b1a5f1f20 --- /dev/null +++ b/src/components/TracePage/index.track.js @@ -0,0 +1,77 @@ +// @flow + +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import _throttle from 'lodash/throttle'; + +import { trackEvent } from '../../utils/tracking'; + +const rangeContext = 'jaeger/ux/trace/range'; +const filterContext = 'jaeger/ux/trace/range'; + +export const FILTER_SET = 'set'; +export const FILTER_CLEAR = 'clear'; + +export const RANGE_REFRAME = 'reframe'; +export const RANGE_SCROLL = 'scroll'; +export const RANGE_SHIFT = 'shift'; + +const rangeCmds = [RANGE_REFRAME, RANGE_SCROLL, RANGE_SHIFT]; + +function trackFilterImpl(cmd: string) { + trackEvent({ + category: filterContext, + action: cmd, + }); +} + +const trackFilterSet = _throttle(() => trackFilterImpl(FILTER_SET), 750, { leading: false }); + +const trackFilterClear = _throttle(() => trackFilterImpl(FILTER_CLEAR), 750, { leading: false }); + +export function trackFilter(value: any) { + if (value) { + trackFilterSet(); + } else { + trackFilterClear(); + } +} + +export function getRangeCmd(current: [number, number], next: [number, number]) { + const [curStart, curEnd] = current; + const [nxStart, nxEnd] = next; + if (curStart === nxStart || curEnd === nxEnd) { + return RANGE_SHIFT; + } + const dStart = (curStart - nxStart).toPrecision(7); + const dEnd = (curEnd - nxEnd).toPrecision(7); + if (dStart === dEnd) { + return RANGE_SHIFT; + } + return RANGE_REFRAME; +} + +export function trackRange(cmd: string, src: string) { + if (rangeCmds.indexOf(cmd) < 0) { + // eslint-disable-next-line no-console + console.error(`Invalid track cmd: ${cmd}`); + return; + } + trackEvent({ + category: rangeContext, + action: cmd, + label: src, + }); +} diff --git a/src/middlewares/index.js b/src/middlewares/index.js index ada8bc0560..a5fbf72d3a 100644 --- a/src/middlewares/index.js +++ b/src/middlewares/index.js @@ -20,6 +20,8 @@ import { replace } from 'react-router-redux'; import { searchTraces, fetchServiceOperations } from '../actions/jaeger-api'; import prefixUrl from '../utils/prefix-url'; +export { default as trackMiddleware } from './track'; + /** * Middleware to load "operations" for a particular service. */ diff --git a/src/middlewares/track.js b/src/middlewares/track.js new file mode 100644 index 0000000000..9aee61e4e4 --- /dev/null +++ b/src/middlewares/track.js @@ -0,0 +1,47 @@ +// @flow + +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { middlewareHooks as spanDetails } from '../components/TracePage/TraceTimelineViewer/SpanDetail/index.track'; +import { middlewareHooks as spanBarRow } from '../components/TracePage/TraceTimelineViewer/SpanBarRow.track'; +import { middlewareHooks as spanDetailRow } from '../components/TracePage/TraceTimelineViewer/SpanDetailRow.track'; +import { middlewareHooks as headerRow } from '../components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineHeaderRow.track'; +import { isGaEnabled } from '../utils/tracking'; + +const trackFns = { ...spanDetails, ...spanBarRow, ...spanDetailRow, ...headerRow }; + +const keysCount = [spanDetails, spanBarRow, spanDetailRow, headerRow].reduce( + (total, middleware) => total + Object.keys(middleware).length, + 0 +); + +if (Object.keys(trackFns).length !== keysCount) { + // eslint-disable-next-line no-console + console.warn('a redux action type has more than one matching tracker middleware'); +} + +function trackingMiddleware(store: { getState: () => any }) { + return function inner(next: any => void) { + return function core(action: any) { + const { type } = action; + if (typeof trackFns[type] === 'function') { + trackFns[type](store, action); + } + return next(action); + }; + }; +} + +export default (isGaEnabled ? trackingMiddleware : undefined); diff --git a/src/types/index.js b/src/types/index.js index 5a6cf31d1d..55ca7d2028 100644 --- a/src/types/index.js +++ b/src/types/index.js @@ -20,12 +20,9 @@ type KeyValuePair = { key: string, - type: string, - value: string, + value: any, }; -export type Tag = KeyValuePair; - export type Log = { timestamp: number, fields: Array, @@ -33,7 +30,7 @@ export type Log = { export type Process = { serviceName: string, - tags: Array, + tags: Array, }; export type SpanReference = { @@ -50,7 +47,7 @@ export type SpanData = { startTime: number, duration: number, logs: Array, - tags: Array, + tags: Array, references: Array, }; diff --git a/src/utils/configure-store.js b/src/utils/configure-store.js index 54e2c708a0..3ddb71d89a 100644 --- a/src/utils/configure-store.js +++ b/src/utils/configure-store.js @@ -29,7 +29,10 @@ export default function configureStore(history) { }), compose( applyMiddleware( - ...[...Object.keys(jaegerMiddlewares).map(key => jaegerMiddlewares[key]), routerMiddleware(history)] + ...Object.keys(jaegerMiddlewares) + .map(key => jaegerMiddlewares[key]) + .filter(Boolean), + routerMiddleware(history) ), process.env.NODE_ENV !== 'production' && window && window.devToolsExtension ? window.devToolsExtension() diff --git a/src/utils/tracking/index.js b/src/utils/tracking/index.js index 70ad413f2a..db43cd20a9 100644 --- a/src/utils/tracking/index.js +++ b/src/utils/tracking/index.js @@ -49,7 +49,7 @@ const isDebugMode = const config = getConfig(); // enable for tests, debug or if in prod with a GA ID -const isGaEnabled = isTest || isDebugMode || (isProd && Boolean(config.gaTrackingID)); +export const isGaEnabled = isTest || isDebugMode || (isProd && Boolean(config.gaTrackingID)); /* istanbul ignore next */ function logTrackingCalls() { From 6132dd4f479dea5172c9da724f3034986d24ba3d Mon Sep 17 00:00:00 2001 From: Joe Farro Date: Thu, 1 Mar 2018 01:36:03 -0500 Subject: [PATCH 12/18] Fix broken tests Signed-off-by: Joe Farro --- src/components/TracePage/SpanGraph/ViewingLayer.test.js | 8 ++++---- .../TimelineHeaderRow/TimelineViewingLayer.test.js | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/TracePage/SpanGraph/ViewingLayer.test.js b/src/components/TracePage/SpanGraph/ViewingLayer.test.js index 7f2d141275..3e574898c8 100644 --- a/src/components/TracePage/SpanGraph/ViewingLayer.test.js +++ b/src/components/TracePage/SpanGraph/ViewingLayer.test.js @@ -141,7 +141,7 @@ describe('', () => { wrapper.instance()._handleReframeDragEnd({ manager, value }); expect(manager.resetBounds.mock.calls).toEqual([[]]); const calls = props.updateViewRangeTime.mock.calls; - expect(calls).toEqual([[value, value]]); + expect(calls).toEqual([[value, value, 'minimap']]); }); it('handles dragged left (anchor is greater)', () => { @@ -154,7 +154,7 @@ describe('', () => { expect(manager.resetBounds.mock.calls).toEqual([[]]); const calls = props.updateViewRangeTime.mock.calls; - expect(calls).toEqual([[value, anchor]]); + expect(calls).toEqual([[value, anchor, 'minimap']]); }); it('handles dragged right (anchor is less)', () => { @@ -167,7 +167,7 @@ describe('', () => { expect(manager.resetBounds.mock.calls).toEqual([[]]); const calls = props.updateViewRangeTime.mock.calls; - expect(calls).toEqual([[anchor, value]]); + expect(calls).toEqual([[anchor, value, 'minimap']]); }); }); }); @@ -251,7 +251,7 @@ describe('', () => { instance._handleScrubberDragEnd(_case.dragUpdate); expect(wrapper.state('preventCursorLine')).toBe(false); expect(manager.resetBounds.mock.calls).toEqual([[]]); - expect(props.updateViewRangeTime).lastCalledWith(..._case.viewRangeUpdate); + expect(props.updateViewRangeTime).lastCalledWith(..._case.viewRangeUpdate, 'minimap'); }); }); }); diff --git a/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineViewingLayer.test.js b/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineViewingLayer.test.js index 63820d9d00..9b14704197 100644 --- a/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineViewingLayer.test.js +++ b/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineViewingLayer.test.js @@ -122,7 +122,7 @@ describe('', () => { wrapper.setProps({ viewRangeTime }); instance._draggerReframe._onDragEnd({ manager, value }); expect(manager.resetBounds.mock.calls).toEqual([[]]); - expect(props.updateViewRangeTime.mock.calls).toEqual([[anchor, shift]]); + expect(props.updateViewRangeTime.mock.calls).toEqual([[anchor, shift, 'timeline-header']]); }); }); From 7b1cd7a5f25c697507a23ba045a9d3c8ab613bc3 Mon Sep 17 00:00:00 2001 From: Joe Farro Date: Thu, 1 Mar 2018 03:14:43 -0500 Subject: [PATCH 13/18] Tests for TracePage tracking Signed-off-by: Joe Farro --- src/components/TracePage/index.track.js | 8 +- src/components/TracePage/index.track.test.js | 151 +++++++++++++++++++ 2 files changed, 156 insertions(+), 3 deletions(-) create mode 100644 src/components/TracePage/index.track.test.js diff --git a/src/components/TracePage/index.track.js b/src/components/TracePage/index.track.js index 0b1a5f1f20..0df1ae4fc1 100644 --- a/src/components/TracePage/index.track.js +++ b/src/components/TracePage/index.track.js @@ -18,12 +18,14 @@ import _throttle from 'lodash/throttle'; import { trackEvent } from '../../utils/tracking'; -const rangeContext = 'jaeger/ux/trace/range'; -const filterContext = 'jaeger/ux/trace/range'; +// export for tests +export const rangeContext = 'jaeger/ux/trace/range'; +export const filterContext = 'jaeger/ux/trace/range'; +// export for tests export const FILTER_SET = 'set'; export const FILTER_CLEAR = 'clear'; - +// export for tests export const RANGE_REFRAME = 'reframe'; export const RANGE_SCROLL = 'scroll'; export const RANGE_SHIFT = 'shift'; diff --git a/src/components/TracePage/index.track.test.js b/src/components/TracePage/index.track.test.js new file mode 100644 index 0000000000..6903491980 --- /dev/null +++ b/src/components/TracePage/index.track.test.js @@ -0,0 +1,151 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* eslint-disable import/first */ + +jest.mock('lodash/throttle', () => jest.fn(fn => fn)); +jest.mock('../../utils/tracking'); + +import _throttle from 'lodash/throttle'; + +import { + FILTER_CLEAR, + FILTER_SET, + filterContext, + getRangeCmd, + RANGE_REFRAME, + RANGE_SHIFT, + rangeContext, + trackFilter, + trackRange, +} from './index.track'; +import { trackEvent } from '../../utils/tracking'; + +describe('trackFilter', () => { + beforeEach(() => { + trackEvent.mockClear(); + }); + + it('uses lodash throttle with 750ms and leading: false', () => { + const calls = _throttle.mock.calls; + expect(calls.length).toBe(2); + expect(calls).toEqual([ + [jasmine.any(Function), 750, { leading: false }], + [jasmine.any(Function), 750, { leading: false }], + ]); + }); + + it('tracks filter set when setting values', () => { + expect(trackEvent.mock.calls.length).toBe(0); + trackFilter('abc'); + expect(trackEvent.mock.calls.length).toBe(1); + expect(trackEvent.mock.calls[0]).toEqual([ + { + category: filterContext, + action: FILTER_SET, + }, + ]); + }); + + it('tracks filter clear when clearing the value', () => { + expect(trackEvent.mock.calls.length).toBe(0); + trackFilter(); + expect(trackEvent.mock.calls.length).toBe(1); + expect(trackEvent.mock.calls[0]).toEqual([ + { + category: filterContext, + action: FILTER_CLEAR, + }, + ]); + }); +}); + +describe('getRangeCmd', () => { + const cases = [ + { + msg: 'returns shift if start is unchanged', + rangeType: RANGE_SHIFT, + args: [[0, 0.5], [0, 0.6]], + }, + { + msg: 'returns shift if end is unchanged', + rangeType: RANGE_SHIFT, + args: [[0, 0.5], [0.1, 0.5]], + }, + { + msg: 'returns shift if increasing start and end by same amount', + rangeType: RANGE_SHIFT, + args: [[0.25, 0.75], [0.5, 1]], + }, + { + msg: 'returns shift if decreasing start and end by same amount', + rangeType: RANGE_SHIFT, + args: [[0.25, 0.75], [0, 0.5]], + }, + { + msg: 'returns reframe if increasing start and end by different amounts', + rangeType: RANGE_REFRAME, + args: [[0.25, 0.75], [0.35, 1]], + }, + { + msg: 'returns reframe if decreasing start and end by different amounts', + rangeType: RANGE_REFRAME, + args: [[0.25, 0.75], [0, 0.65]], + }, + { + msg: 'returns reframe when widening to a superset', + rangeType: RANGE_REFRAME, + args: [[0.25, 0.75], [0, 1]], + }, + { + msg: 'returns reframe when contracting to a subset', + rangeType: RANGE_REFRAME, + args: [[0.25, 0.75], [0.45, 0.55]], + }, + ]; + + cases.forEach(_case => { + const { msg, rangeType, args } = _case; + it(msg, () => { + const rv = getRangeCmd(...args); + expect(rv).toBe(rangeType); + }); + }); +}); + +describe('trackRange', () => { + beforeEach(() => { + trackEvent.mockClear(); + }); + + it('does nothing if given an invalid command', () => { + expect(trackEvent.mock.calls.length).toBe(0); + trackRange('invalid-command', 'abc'); + expect(trackEvent.mock.calls.length).toBe(0); + }); + + it('tracks the given context, command and source', () => { + const source = 'some-source'; + expect(trackEvent.mock.calls.length).toBe(0); + trackRange(RANGE_SHIFT, source); + expect(trackEvent.mock.calls.length).toBe(1); + expect(trackEvent.mock.calls[0]).toEqual([ + { + category: rangeContext, + action: RANGE_SHIFT, + label: source, + }, + ]); + }); +}); From ce467f4dffcd9173531b9da151a09427fb095f34 Mon Sep 17 00:00:00 2001 From: Joe Farro Date: Thu, 1 Mar 2018 16:31:23 -0500 Subject: [PATCH 14/18] Additional tests for trace GA event tracking Signed-off-by: Joe Farro --- .../TracePage/TracePageHeader.track.js | 2 +- .../SpanDetail/index.track.js | 17 ++-- .../SpanDetail/index.track.test.js | 90 +++++++++++++++++++ src/components/TracePage/index.test.js | 49 +++++++++- 4 files changed, 148 insertions(+), 10 deletions(-) create mode 100644 src/components/TracePage/TraceTimelineViewer/SpanDetail/index.track.test.js diff --git a/src/components/TracePage/TracePageHeader.track.js b/src/components/TracePage/TracePageHeader.track.js index 68e50e50a9..8215b182f3 100644 --- a/src/components/TracePage/TracePageHeader.track.js +++ b/src/components/TracePage/TracePageHeader.track.js @@ -17,7 +17,7 @@ import { trackEvent } from '../../utils/tracking'; const altViewCtx = 'jaeger/ux/trace/alt-view'; -const slimHeaderCtx = 'jaeger/ux/trace/slim-header'; +export const slimHeaderCtx = 'jaeger/ux/trace/slim-header'; export function trackAltView() { trackEvent({ diff --git a/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.track.js b/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.track.js index f2ce2662e3..6f7aa0152f 100644 --- a/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.track.js +++ b/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.track.js @@ -19,37 +19,38 @@ import { trackEvent } from '../../../../utils/tracking'; const baseContext = 'jaeger/ux/trace/timeline'; -const tagsContext = `${baseContext}/tags`; -const processContext = `${baseContext}/process`; -const logsContext = `${baseContext}/logs`; -const logsItemContext = `${baseContext}/logs/item`; +// export for tests +export const tagsContext = `${baseContext}/tags`; +export const processContext = `${baseContext}/process`; +export const logsContext = `${baseContext}/logs`; +export const logsItemContext = `${baseContext}/logs/item`; function getCmd(isOpen: boolean) { return isOpen ? 'open' : 'close'; } -export function logs(isOpen: boolean) { +function logs(isOpen: boolean) { trackEvent({ category: logsContext, action: getCmd(isOpen), }); } -export function logsItem(isOpen: boolean) { +function logsItem(isOpen: boolean) { trackEvent({ category: logsItemContext, action: getCmd(isOpen), }); } -export function process(isOpen: boolean) { +function process(isOpen: boolean) { trackEvent({ category: processContext, action: getCmd(isOpen), }); } -export function tags(isOpen: boolean) { +function tags(isOpen: boolean) { trackEvent({ category: tagsContext, action: getCmd(isOpen), diff --git a/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.track.test.js b/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.track.test.js new file mode 100644 index 0000000000..50e8f01dde --- /dev/null +++ b/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.track.test.js @@ -0,0 +1,90 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* eslint-disable import/first */ +jest.mock('../../../../utils/tracking'); + +import DetailState from './DetailState'; +import * as track from './index.track'; +import { actionTypes as types } from '../duck'; +import { trackEvent } from '../../../../utils/tracking'; + +describe('middlewareHooks', () => { + const spanID = 'abc'; + const state = { + traceTimeline: { + detailStates: new Map([[spanID, new DetailState()]]), + }, + }; + const store = { + getState() { + return state; + }, + }; + const payload = { spanID }; + + beforeEach(() => { + trackEvent.mockClear(); + }); + + const cases = [ + { + msg: 'tracks a GA event for toggling the span tags', + type: types.DETAIL_TAGS_TOGGLE, + category: track.tagsContext, + }, + { + msg: 'tracks a GA event for toggling the span tags', + type: types.DETAIL_PROCESS_TOGGLE, + category: track.processContext, + }, + { + msg: 'tracks a GA event for toggling the span logs view', + type: types.DETAIL_LOGS_TOGGLE, + category: track.logsContext, + }, + { + msg: 'tracks a GA event for toggling the span logs view', + type: types.DETAIL_LOG_ITEM_TOGGLE, + payloadCustom: { ...payload, logItem: {} }, + category: track.logsItemContext, + }, + ]; + + it('has the correct keys and they refer to functions', () => { + expect(Object.keys(track.middlewareHooks).sort()).toEqual( + [ + types.DETAIL_TAGS_TOGGLE, + types.DETAIL_PROCESS_TOGGLE, + types.DETAIL_LOGS_TOGGLE, + types.DETAIL_LOG_ITEM_TOGGLE, + ].sort() + ); + }); + + cases.forEach(_case => { + const { msg, type, category, payloadCustom = null } = _case; + it(msg, () => { + const action = { type, payload: payloadCustom || payload }; + track.middlewareHooks[type](store, action); + expect(trackEvent.mock.calls.length).toBe(1); + expect(trackEvent.mock.calls[0]).toEqual([ + { + category, + action: jasmine.any(String), + }, + ]); + }); + }); +}); diff --git a/src/components/TracePage/index.test.js b/src/components/TracePage/index.test.js index 09a253bb87..ff0c2e9368 100644 --- a/src/components/TracePage/index.test.js +++ b/src/components/TracePage/index.test.js @@ -12,13 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. +/* eslint-disable import/first */ +jest.mock('./index.track'); jest.mock('./keyboard-shortcuts'); jest.mock('./scroll-page'); // mock these to enable mount() jest.mock('./SpanGraph'); +jest.mock('./TracePageHeader.track'); jest.mock('./TraceTimelineViewer'); -/* eslint-disable import/first */ import React from 'react'; import sinon from 'sinon'; import { shallow, mount } from 'enzyme'; @@ -30,10 +32,12 @@ import TracePage, { shortcutConfig, VIEW_MIN_RANGE, } from './index'; +import * as track from './index.track'; import { reset as resetShortcuts } from './keyboard-shortcuts'; import { cancel as cancelScroll } from './scroll-page'; import SpanGraph from './SpanGraph'; import TracePageHeader from './TracePageHeader'; +import { trackSlimHeader } from './TracePageHeader.track'; import TraceTimelineViewer from './TraceTimelineViewer'; import ErrorMessage from '../common/ErrorMessage'; import LoadingIndicator from '../common/LoadingIndicator'; @@ -288,6 +292,49 @@ describe('', () => { expect(timeline.prop('viewRange')).toEqual(viewRange); }); }); + + describe('GA tracking', () => { + let header; + let spanGraph; + + function refreshWrappers() { + header = wrapper.find(TracePageHeader); + spanGraph = wrapper.find(SpanGraph); + } + + beforeEach(() => { + wrapper = mount(); + // use the method directly because it is a `ref` prop + wrapper.instance().setHeaderHeight({ clientHeight: 1 }); + wrapper.update(); + refreshWrappers(); + }); + + it('tracks setting the header to slim-view', () => { + const { onSlimViewClicked } = header.props(); + trackSlimHeader.mockReset(); + onSlimViewClicked(true); + onSlimViewClicked(false); + expect(trackSlimHeader.mock.calls).toEqual([[false], [true]]); + }); + + it('tracks setting or clearing the filter', () => { + const { updateTextFilter } = header.props(); + track.trackFilter.mockClear(); + updateTextFilter('abc'); + updateTextFilter(''); + expect(track.trackFilter.mock.calls).toEqual([['abc'], ['']]); + }); + + it('tracks changes to the viewRange', () => { + const { updateViewRangeTime } = spanGraph.props(); + track.getRangeCmd = jest.fn(() => track.RANGE_REFRAME); + track.trackRange.mockClear(); + const range = [0.25, 0.75]; + updateViewRangeTime(...range, 'some-source'); + expect(track.getRangeCmd.mock.calls).toEqual([[range, [0, 1]]]); + }); + }); }); describe('mapDispatchToProps()', () => { From d7497672bfd505cabe961f8a461225f5a9898a0c Mon Sep 17 00:00:00 2001 From: Joe Farro Date: Tue, 6 Mar 2018 10:08:21 -0500 Subject: [PATCH 15/18] Better names for tracking functions Signed-off-by: Joe Farro --- src/components/TracePage/KeyboardShortcutsHelp.track.js | 2 +- src/components/TracePage/TracePageHeader.js | 6 +++--- src/components/TracePage/TracePageHeader.track.js | 4 ++-- src/components/TracePage/index.js | 4 ++-- src/components/TracePage/index.test.js | 6 +++--- src/index.js | 6 +++--- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/components/TracePage/KeyboardShortcutsHelp.track.js b/src/components/TracePage/KeyboardShortcutsHelp.track.js index 40236e1628..00c26d9f3f 100644 --- a/src/components/TracePage/KeyboardShortcutsHelp.track.js +++ b/src/components/TracePage/KeyboardShortcutsHelp.track.js @@ -18,7 +18,7 @@ import { trackEvent } from '../../utils/tracking'; const context = 'jaeger/ux/trace/kbd-modal'; -export default function trackKbdHelpModal() { +export default function trackKbdHelpModalOpen() { trackEvent({ category: context, action: 'open', diff --git a/src/components/TracePage/TracePageHeader.js b/src/components/TracePage/TracePageHeader.js index fa85378b8a..f3817cdcc5 100644 --- a/src/components/TracePage/TracePageHeader.js +++ b/src/components/TracePage/TracePageHeader.js @@ -21,7 +21,7 @@ import IoChevronRight from 'react-icons/lib/io/chevron-right'; import { Link } from 'react-router-dom'; import * as markers from './TracePageHeader.markers'; -import { trackAltView } from './TracePageHeader.track'; +import { trackAltViewOpen } from './TracePageHeader.track'; import KeyboardShortcutsHelp from './KeyboardShortcutsHelp'; import LabeledList from '../common/LabeledList'; import { FALLBACK_TRACE_NAME } from '../../constants'; @@ -109,7 +109,7 @@ export default function TracePageHeader(props: TracePageHeaderProps) { to={prefixUrl(`/api/traces/${traceID}`)} rel="noopener noreferrer" target="_blank" - onClick={trackAltView} + onClick={trackAltViewOpen} > Trace JSON @@ -119,7 +119,7 @@ export default function TracePageHeader(props: TracePageHeaderProps) { to={prefixUrl(`/api/traces/${traceID}?raw=true`)} rel="noopener noreferrer" target="_blank" - onClick={trackAltView} + onClick={trackAltViewOpen} > Trace JSON (unadjusted) diff --git a/src/components/TracePage/TracePageHeader.track.js b/src/components/TracePage/TracePageHeader.track.js index 8215b182f3..340af09f38 100644 --- a/src/components/TracePage/TracePageHeader.track.js +++ b/src/components/TracePage/TracePageHeader.track.js @@ -19,14 +19,14 @@ import { trackEvent } from '../../utils/tracking'; const altViewCtx = 'jaeger/ux/trace/alt-view'; export const slimHeaderCtx = 'jaeger/ux/trace/slim-header'; -export function trackAltView() { +export function trackAltViewOpen() { trackEvent({ category: altViewCtx, action: 'open', }); } -export function trackSlimHeader(isOpen: boolean) { +export function trackSlimHeaderToggle(isOpen: boolean) { trackEvent({ category: slimHeaderCtx, action: isOpen ? 'open' : 'close', diff --git a/src/components/TracePage/index.js b/src/components/TracePage/index.js index de5fb95e9e..db44806b5e 100644 --- a/src/components/TracePage/index.js +++ b/src/components/TracePage/index.js @@ -30,7 +30,7 @@ import { cancel as cancelScroll, scrollBy, scrollTo } from './scroll-page'; import ScrollManager from './ScrollManager'; import SpanGraph from './SpanGraph'; import TracePageHeader from './TracePageHeader'; -import { trackSlimHeader } from './TracePageHeader.track'; +import { trackSlimHeaderToggle } from './TracePageHeader.track'; import TraceTimelineViewer from './TraceTimelineViewer'; import type { ViewRange, ViewRangeTimeUpdate } from './types'; import ErrorMessage from '../common/ErrorMessage'; @@ -215,7 +215,7 @@ export default class TracePage extends React.PureComponent { const { slimView } = this.state; - trackSlimHeader(slimView); + trackSlimHeaderToggle(slimView); this.setState({ slimView: !slimView }); }; diff --git a/src/components/TracePage/index.test.js b/src/components/TracePage/index.test.js index ff0c2e9368..4ff829e70a 100644 --- a/src/components/TracePage/index.test.js +++ b/src/components/TracePage/index.test.js @@ -37,7 +37,7 @@ import { reset as resetShortcuts } from './keyboard-shortcuts'; import { cancel as cancelScroll } from './scroll-page'; import SpanGraph from './SpanGraph'; import TracePageHeader from './TracePageHeader'; -import { trackSlimHeader } from './TracePageHeader.track'; +import { trackSlimHeaderToggle } from './TracePageHeader.track'; import TraceTimelineViewer from './TraceTimelineViewer'; import ErrorMessage from '../common/ErrorMessage'; import LoadingIndicator from '../common/LoadingIndicator'; @@ -312,10 +312,10 @@ describe('', () => { it('tracks setting the header to slim-view', () => { const { onSlimViewClicked } = header.props(); - trackSlimHeader.mockReset(); + trackSlimHeaderToggle.mockReset(); onSlimViewClicked(true); onSlimViewClicked(false); - expect(trackSlimHeader.mock.calls).toEqual([[false], [true]]); + expect(trackSlimHeaderToggle.mock.calls).toEqual([[false], [true]]); }); it('tracks setting or clearing the filter', () => { diff --git a/src/index.js b/src/index.js index 12469488bf..ba9874a533 100644 --- a/src/index.js +++ b/src/index.js @@ -16,6 +16,9 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { document } from 'global'; +import JaegerUIApp from './components/App'; +import { context as trackingContext } from './utils/tracking'; + // these need to go after the App import /* eslint-disable import/first */ import 'u-basscss/css/flexbox.css'; @@ -25,9 +28,6 @@ import 'u-basscss/css/padding.css'; import 'u-basscss/css/position.css'; import 'u-basscss/css/typography.css'; -import JaegerUIApp from './components/App'; -import { context as trackingContext } from './utils/tracking'; - const UI_ROOT_ID = 'jaeger-ui-root'; if (trackingContext) { From b3ea6fb38ce1682958151cb55bb3e2e49aa46859 Mon Sep 17 00:00:00 2001 From: Joe Farro Date: Thu, 15 Mar 2018 20:09:29 -0400 Subject: [PATCH 16/18] Make GA event tracking more concise (PR feedback) Signed-off-by: Joe Farro --- .../TracePage/KeyboardShortcutsHelp.track.js | 12 +- .../TracePage/TracePageHeader.track.js | 21 +-- .../TraceTimelineViewer/SpanBarRow.track.js | 42 ------ .../SpanDetail/index.track.js | 71 ----------- .../SpanDetailRow.track.js | 32 ----- .../TraceTimelineViewer/duck.track.js | 78 ++++++++++++ ...index.track.test.js => duck.track.test.js} | 82 ++++++++---- src/components/TracePage/index.js | 9 +- src/components/TracePage/index.test.js | 8 +- src/components/TracePage/index.track.js | 62 +++------ src/components/TracePage/index.track.test.js | 120 ++++++++---------- src/middlewares/track.js | 21 +-- src/utils/get-last-xform-cacher.test.js | 2 +- .../tracking/common.js} | 19 +-- src/utils/tracking/get-event-tracker.js | 46 +++++++ src/utils/tracking/index.js | 61 ++++----- src/utils/tracking/index.test.js | 29 +++-- 17 files changed, 316 insertions(+), 399 deletions(-) delete mode 100644 src/components/TracePage/TraceTimelineViewer/SpanBarRow.track.js delete mode 100644 src/components/TracePage/TraceTimelineViewer/SpanDetail/index.track.js delete mode 100644 src/components/TracePage/TraceTimelineViewer/SpanDetailRow.track.js create mode 100644 src/components/TracePage/TraceTimelineViewer/duck.track.js rename src/components/TracePage/TraceTimelineViewer/{SpanDetail/index.track.test.js => duck.track.test.js} (56%) rename src/{components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineHeaderRow.track.js => utils/tracking/common.js} (58%) create mode 100644 src/utils/tracking/get-event-tracker.js diff --git a/src/components/TracePage/KeyboardShortcutsHelp.track.js b/src/components/TracePage/KeyboardShortcutsHelp.track.js index 00c26d9f3f..7472d04a38 100644 --- a/src/components/TracePage/KeyboardShortcutsHelp.track.js +++ b/src/components/TracePage/KeyboardShortcutsHelp.track.js @@ -14,13 +14,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { trackEvent } from '../../utils/tracking'; +import { OPEN } from '../../utils/tracking/common'; +import getEventTracker from '../../utils/tracking/get-event-tracker'; -const context = 'jaeger/ux/trace/kbd-modal'; +const CATEGORY = 'jaeger/ux/trace/kbd-modal'; -export default function trackKbdHelpModalOpen() { - trackEvent({ - category: context, - action: 'open', - }); -} +export default getEventTracker(CATEGORY, OPEN); diff --git a/src/components/TracePage/TracePageHeader.track.js b/src/components/TracePage/TracePageHeader.track.js index 340af09f38..016c3308ab 100644 --- a/src/components/TracePage/TracePageHeader.track.js +++ b/src/components/TracePage/TracePageHeader.track.js @@ -14,21 +14,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { trackEvent } from '../../utils/tracking'; +import { getToggleValue, OPEN } from '../../utils/tracking/common'; +import getEventTracker from '../../utils/tracking/get-event-tracker'; -const altViewCtx = 'jaeger/ux/trace/alt-view'; -export const slimHeaderCtx = 'jaeger/ux/trace/slim-header'; +const CATEGORY_ALT_VIEW = 'jaeger/ux/trace/alt-view'; +const CATEGORY_SLIM_HEADER = 'jaeger/ux/trace/slim-header'; -export function trackAltViewOpen() { - trackEvent({ - category: altViewCtx, - action: 'open', - }); -} +export const trackAltViewOpen = getEventTracker(CATEGORY_ALT_VIEW, OPEN); -export function trackSlimHeaderToggle(isOpen: boolean) { - trackEvent({ - category: slimHeaderCtx, - action: isOpen ? 'open' : 'close', - }); -} +export const trackSlimHeaderToggle = getEventTracker(CATEGORY_SLIM_HEADER, getToggleValue); diff --git a/src/components/TracePage/TraceTimelineViewer/SpanBarRow.track.js b/src/components/TracePage/TraceTimelineViewer/SpanBarRow.track.js deleted file mode 100644 index ec97e68c00..0000000000 --- a/src/components/TracePage/TraceTimelineViewer/SpanBarRow.track.js +++ /dev/null @@ -1,42 +0,0 @@ -// @flow - -// Copyright (c) 2017 Uber Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import type { Store } from 'redux'; - -import { actionTypes as types } from './duck'; -import { trackEvent } from '../../../utils/tracking'; - -const context = 'jaeger/ux/trace/timeline/parent'; - -function trackParent(store: Store, action: any) { - const st = store.getState(); - const { spanID } = action.payload; - const traceID = st.traceTimeline.traceID; - const isHidden = st.traceTimeline.childrenHiddenIDs.has(spanID); - const span = st.trace.traces[traceID].spans.find(sp => sp.spanID === spanID); - if (span) { - trackEvent({ - category: context, - action: isHidden ? 'open' : 'close', - value: span.depth, - }); - } -} - -// eslint-disable-next-line import/prefer-default-export -export const middlewareHooks = { - [types.CHILDREN_TOGGLE]: trackParent, -}; diff --git a/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.track.js b/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.track.js deleted file mode 100644 index 6f7aa0152f..0000000000 --- a/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.track.js +++ /dev/null @@ -1,71 +0,0 @@ -// @flow - -// Copyright (c) 2017 Uber Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { actionTypes as types } from '../duck'; -import { trackEvent } from '../../../../utils/tracking'; - -const baseContext = 'jaeger/ux/trace/timeline'; - -// export for tests -export const tagsContext = `${baseContext}/tags`; -export const processContext = `${baseContext}/process`; -export const logsContext = `${baseContext}/logs`; -export const logsItemContext = `${baseContext}/logs/item`; - -function getCmd(isOpen: boolean) { - return isOpen ? 'open' : 'close'; -} - -function logs(isOpen: boolean) { - trackEvent({ - category: logsContext, - action: getCmd(isOpen), - }); -} - -function logsItem(isOpen: boolean) { - trackEvent({ - category: logsItemContext, - action: getCmd(isOpen), - }); -} - -function process(isOpen: boolean) { - trackEvent({ - category: processContext, - action: getCmd(isOpen), - }); -} - -function tags(isOpen: boolean) { - trackEvent({ - category: tagsContext, - action: getCmd(isOpen), - }); -} - -const getDetail = (store, action) => store.getState().traceTimeline.detailStates.get(action.payload.spanID); - -export const middlewareHooks = { - [types.DETAIL_TAGS_TOGGLE]: (store, action) => tags(!getDetail(store, action).isTagsOpen), - [types.DETAIL_PROCESS_TOGGLE]: (store, action) => process(!getDetail(store, action).isProcessOpen), - [types.DETAIL_LOGS_TOGGLE]: (store, action) => logs(!getDetail(store, action).logs.isOpen), - [types.DETAIL_LOG_ITEM_TOGGLE]: (store, action) => { - const detail = getDetail(store, action); - const { logItem } = action.payload; - logsItem(!detail.logs.openedItems.has(logItem)); - }, -}; diff --git a/src/components/TracePage/TraceTimelineViewer/SpanDetailRow.track.js b/src/components/TracePage/TraceTimelineViewer/SpanDetailRow.track.js deleted file mode 100644 index 71623c1375..0000000000 --- a/src/components/TracePage/TraceTimelineViewer/SpanDetailRow.track.js +++ /dev/null @@ -1,32 +0,0 @@ -// @flow - -// Copyright (c) 2017 Uber Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { actionTypes as types } from './duck'; -import { trackEvent } from '../../../utils/tracking'; - -const context = 'jaeger/ux/trace/timeline/row'; - -export default function trackRow(isOpen: boolean) { - trackEvent({ - category: context, - action: isOpen ? 'open' : 'close', - }); -} - -export const middlewareHooks = { - [types.DETAIL_TOGGLE]: (store, action) => - trackRow(!store.getState().traceTimeline.detailStates.has(action.payload.spanID)), -}; diff --git a/src/components/TracePage/TraceTimelineViewer/duck.track.js b/src/components/TracePage/TraceTimelineViewer/duck.track.js new file mode 100644 index 0000000000..7e275da57a --- /dev/null +++ b/src/components/TracePage/TraceTimelineViewer/duck.track.js @@ -0,0 +1,78 @@ +// @flow + +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import _get from 'lodash/get'; + +import type { Store } from 'redux'; + +import { actionTypes as types } from './duck'; +import { trackEvent } from '../../../utils/tracking'; +import { getToggleValue } from '../../../utils/tracking/common'; +import getEventTracker from '../../../utils/tracking/get-event-tracker'; + +const ACTION_RESIZE = 'resize'; + +const CATEGORY_BASE = 'jaeger/ux/trace/timeline'; +// export for tests +export const CATEGORY_TAGS = `${CATEGORY_BASE}/tags`; +export const CATEGORY_PROCESS = `${CATEGORY_BASE}/process`; +export const CATEGORY_LOGS = `${CATEGORY_BASE}/logs`; +export const CATEGORY_LOGS_ITEM = `${CATEGORY_BASE}/logs/item`; +export const CATEGORY_COLUMN = `${CATEGORY_BASE}/column`; +export const CATEGORY_PARENT = `${CATEGORY_BASE}parent`; +export const CATEGORY_ROW = `${CATEGORY_BASE}/row`; + +const isLogItemOpen = (openedItems, reduxAction) => openedItems.has(reduxAction.payload.logItem); + +function getDetailStateConverter(detailPath, transformer: (...any) => boolean = Boolean) { + return function getAction(store, reduxAction) { + const { spanID } = reduxAction.payload; + const detail = store.getState().traceTimeline.detailStates.get(spanID); + const detailItem = _get(detail, detailPath); + return getToggleValue(transformer(detailItem, reduxAction)); + }; +} + +const getColumnWidth = (_, action): number => Math.round(action.payload.width * 1000); + +function getDetailRowToggleAction(store, action) { + const detailIsOpen = store.getState().traceTimeline.detailStates.has(action.payload.spanID); + return getToggleValue(detailIsOpen); +} + +function trackParent(store: Store, action: any) { + const st = store.getState(); + const { spanID } = action.payload; + const traceID = st.traceTimeline.traceID; + const isHidden = st.traceTimeline.childrenHiddenIDs.has(spanID); + const span = st.trace.traces[traceID].spans.find(sp => sp.spanID === spanID); + if (span) { + trackEvent(CATEGORY_PARENT, getToggleValue(!isHidden), span.depth); + } +} + +export const middlewareHooks = { + [types.CHILDREN_TOGGLE]: trackParent, + [types.DETAIL_TOGGLE]: getEventTracker(CATEGORY_ROW, getDetailRowToggleAction), + [types.DETAIL_TAGS_TOGGLE]: getEventTracker(CATEGORY_TAGS, getDetailStateConverter('isTagsOpen')), + [types.DETAIL_PROCESS_TOGGLE]: getEventTracker(CATEGORY_PROCESS, getDetailStateConverter('isProcessOpen')), + [types.DETAIL_LOGS_TOGGLE]: getEventTracker(CATEGORY_LOGS, getDetailStateConverter('logs.isOpen')), + [types.DETAIL_LOG_ITEM_TOGGLE]: getEventTracker( + CATEGORY_LOGS_ITEM, + getDetailStateConverter('logs.openedItems', isLogItemOpen) + ), + [types.SET_SPAN_NAME_COLUMN_WIDTH]: getEventTracker(CATEGORY_COLUMN, ACTION_RESIZE, getColumnWidth), +}; diff --git a/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.track.test.js b/src/components/TracePage/TraceTimelineViewer/duck.track.test.js similarity index 56% rename from src/components/TracePage/TraceTimelineViewer/SpanDetail/index.track.test.js rename to src/components/TracePage/TraceTimelineViewer/duck.track.test.js index 50e8f01dde..87950d2b7c 100644 --- a/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.track.test.js +++ b/src/components/TracePage/TraceTimelineViewer/duck.track.test.js @@ -13,17 +13,30 @@ // limitations under the License. /* eslint-disable import/first */ -jest.mock('../../../../utils/tracking'); +jest.mock('../../../utils/tracking'); -import DetailState from './DetailState'; -import * as track from './index.track'; -import { actionTypes as types } from '../duck'; -import { trackEvent } from '../../../../utils/tracking'; +import DetailState from './SpanDetail/DetailState'; +import * as track from './duck.track'; +import { actionTypes as types } from './duck'; +import { trackEvent } from '../../../utils/tracking'; describe('middlewareHooks', () => { + const traceID = 'ABC'; const spanID = 'abc'; + const spanDepth = 123; + const columnWidth = { real: 0.15, tracked: 150 }; + const payload = { spanID }; const state = { + trace: { + traces: { + [traceID]: { + spans: [{ spanID, depth: spanDepth }], + }, + }, + }, traceTimeline: { + traceID, + childrenHiddenIDs: new Map(), detailStates: new Map([[spanID, new DetailState()]]), }, }; @@ -32,59 +45,72 @@ describe('middlewareHooks', () => { return state; }, }; - const payload = { spanID }; - beforeEach(() => { - trackEvent.mockClear(); - }); + beforeEach(trackEvent.mockClear); const cases = [ + { + msg: 'tracks a GA event for resizing the span name column', + type: types.SET_SPAN_NAME_COLUMN_WIDTH, + payloadCustom: { width: columnWidth.real }, + category: track.CATEGORY_COLUMN, + extraTrackArgs: [columnWidth.tracked], + }, + { + msg: 'tracks a GA event for collapsing a parent', + type: types.CHILDREN_TOGGLE, + category: track.CATEGORY_PARENT, + extraTrackArgs: [123], + }, + { + msg: 'tracks a GA event for toggling a detail row', + type: types.DETAIL_TOGGLE, + category: track.CATEGORY_ROW, + }, { msg: 'tracks a GA event for toggling the span tags', type: types.DETAIL_TAGS_TOGGLE, - category: track.tagsContext, + category: track.CATEGORY_TAGS, }, { msg: 'tracks a GA event for toggling the span tags', type: types.DETAIL_PROCESS_TOGGLE, - category: track.processContext, + category: track.CATEGORY_PROCESS, }, { msg: 'tracks a GA event for toggling the span logs view', type: types.DETAIL_LOGS_TOGGLE, - category: track.logsContext, + category: track.CATEGORY_LOGS, }, { msg: 'tracks a GA event for toggling the span logs view', type: types.DETAIL_LOG_ITEM_TOGGLE, payloadCustom: { ...payload, logItem: {} }, - category: track.logsItemContext, + category: track.CATEGORY_LOGS_ITEM, }, ]; + cases.forEach(_case => { + const { msg, type, category, extraTrackArgs = [], payloadCustom = null } = _case; + it(msg, () => { + const action = { type, payload: payloadCustom || payload }; + track.middlewareHooks[type](store, action); + expect(trackEvent.mock.calls.length).toBe(1); + expect(trackEvent.mock.calls[0]).toEqual([category, expect.any(String), ...extraTrackArgs]); + }); + }); + it('has the correct keys and they refer to functions', () => { expect(Object.keys(track.middlewareHooks).sort()).toEqual( [ + types.CHILDREN_TOGGLE, + types.DETAIL_TOGGLE, types.DETAIL_TAGS_TOGGLE, types.DETAIL_PROCESS_TOGGLE, types.DETAIL_LOGS_TOGGLE, types.DETAIL_LOG_ITEM_TOGGLE, + types.SET_SPAN_NAME_COLUMN_WIDTH, ].sort() ); }); - - cases.forEach(_case => { - const { msg, type, category, payloadCustom = null } = _case; - it(msg, () => { - const action = { type, payload: payloadCustom || payload }; - track.middlewareHooks[type](store, action); - expect(trackEvent.mock.calls.length).toBe(1); - expect(trackEvent.mock.calls[0]).toEqual([ - { - category, - action: jasmine.any(String), - }, - ]); - }); - }); }); diff --git a/src/components/TracePage/index.js b/src/components/TracePage/index.js index db44806b5e..b756e66a48 100644 --- a/src/components/TracePage/index.js +++ b/src/components/TracePage/index.js @@ -23,7 +23,7 @@ import { connect } from 'react-redux'; import type { RouterHistory, Match } from 'react-router-dom'; import { bindActionCreators } from 'redux'; -import * as track from './index.track'; +import { trackFilter, trackRange } from './index.track'; import type { CombokeysHandler, ShortcutCallbacks } from './keyboard-shortcuts'; import { init as initShortcuts, reset as resetShortcuts } from './keyboard-shortcuts'; import { cancel as cancelScroll, scrollBy, scrollTo } from './scroll-page'; @@ -193,14 +193,13 @@ export default class TracePage extends React.PureComponent { - track.trackFilter(textFilter); + trackFilter(textFilter); this.setState({ textFilter }); }; updateViewRangeTime = (start: number, end: number, trackSrc?: string) => { if (trackSrc) { - const trackCmd = track.getRangeCmd([start, end], this.state.viewRange.time.current); - track.trackRange(trackCmd, trackSrc); + trackRange(trackSrc, [start, end], this.state.viewRange.time.current); } const time = { current: [start, end] }; const viewRange = { ...this.state.viewRange, time }; @@ -215,7 +214,7 @@ export default class TracePage extends React.PureComponent { const { slimView } = this.state; - trackSlimHeaderToggle(slimView); + trackSlimHeaderToggle(!slimView); this.setState({ slimView: !slimView }); }; diff --git a/src/components/TracePage/index.test.js b/src/components/TracePage/index.test.js index 4ff829e70a..151a27372f 100644 --- a/src/components/TracePage/index.test.js +++ b/src/components/TracePage/index.test.js @@ -315,7 +315,7 @@ describe('', () => { trackSlimHeaderToggle.mockReset(); onSlimViewClicked(true); onSlimViewClicked(false); - expect(trackSlimHeaderToggle.mock.calls).toEqual([[false], [true]]); + expect(trackSlimHeaderToggle.mock.calls).toEqual([[true], [false]]); }); it('tracks setting or clearing the filter', () => { @@ -327,12 +327,12 @@ describe('', () => { }); it('tracks changes to the viewRange', () => { + const src = 'some-source'; const { updateViewRangeTime } = spanGraph.props(); - track.getRangeCmd = jest.fn(() => track.RANGE_REFRAME); track.trackRange.mockClear(); const range = [0.25, 0.75]; - updateViewRangeTime(...range, 'some-source'); - expect(track.getRangeCmd.mock.calls).toEqual([[range, [0, 1]]]); + updateViewRangeTime(...range, src); + expect(track.trackRange.mock.calls).toEqual([[src, range, [0, 1]]]); }); }); }); diff --git a/src/components/TracePage/index.track.js b/src/components/TracePage/index.track.js index 0df1ae4fc1..84d0313063 100644 --- a/src/components/TracePage/index.track.js +++ b/src/components/TracePage/index.track.js @@ -16,64 +16,40 @@ import _throttle from 'lodash/throttle'; -import { trackEvent } from '../../utils/tracking'; +import getEventTracker from '../../utils/tracking/get-event-tracker'; // export for tests -export const rangeContext = 'jaeger/ux/trace/range'; -export const filterContext = 'jaeger/ux/trace/range'; +export const CATEGORY_RANGE = 'jaeger/ux/trace/range'; +export const CATEGORY_FILTER = 'jaeger/ux/trace/range'; // export for tests -export const FILTER_SET = 'set'; -export const FILTER_CLEAR = 'clear'; -// export for tests -export const RANGE_REFRAME = 'reframe'; -export const RANGE_SCROLL = 'scroll'; -export const RANGE_SHIFT = 'shift'; - -const rangeCmds = [RANGE_REFRAME, RANGE_SCROLL, RANGE_SHIFT]; - -function trackFilterImpl(cmd: string) { - trackEvent({ - category: filterContext, - action: cmd, - }); -} +export const ACTION_FILTER_SET = 'set'; +export const ACTION_FILTER_CLEAR = 'clear'; +export const ACTION_RANGE_REFRAME = 'reframe'; +export const ACTION_RANGE_SHIFT = 'shift'; -const trackFilterSet = _throttle(() => trackFilterImpl(FILTER_SET), 750, { leading: false }); +const trackFilterSet = _throttle(getEventTracker(CATEGORY_FILTER, ACTION_FILTER_SET), 750, { + leading: false, +}); -const trackFilterClear = _throttle(() => trackFilterImpl(FILTER_CLEAR), 750, { leading: false }); +const trackFilterClear = _throttle(getEventTracker(CATEGORY_FILTER, ACTION_FILTER_CLEAR), 750, { + leading: false, +}); -export function trackFilter(value: any) { - if (value) { - trackFilterSet(); - } else { - trackFilterClear(); - } -} +export const trackFilter = (value: any) => (value ? trackFilterSet() : trackFilterClear()); -export function getRangeCmd(current: [number, number], next: [number, number]) { +function getRangeAction(_: string, current: [number, number], next: [number, number]) { const [curStart, curEnd] = current; const [nxStart, nxEnd] = next; if (curStart === nxStart || curEnd === nxEnd) { - return RANGE_SHIFT; + return ACTION_RANGE_SHIFT; } const dStart = (curStart - nxStart).toPrecision(7); const dEnd = (curEnd - nxEnd).toPrecision(7); if (dStart === dEnd) { - return RANGE_SHIFT; + return ACTION_RANGE_SHIFT; } - return RANGE_REFRAME; + return ACTION_RANGE_REFRAME; } -export function trackRange(cmd: string, src: string) { - if (rangeCmds.indexOf(cmd) < 0) { - // eslint-disable-next-line no-console - console.error(`Invalid track cmd: ${cmd}`); - return; - } - trackEvent({ - category: rangeContext, - action: cmd, - label: src, - }); -} +export const trackRange = getEventTracker(CATEGORY_RANGE, getRangeAction, String); diff --git a/src/components/TracePage/index.track.test.js b/src/components/TracePage/index.track.test.js index 6903491980..a8d2dad23a 100644 --- a/src/components/TracePage/index.track.test.js +++ b/src/components/TracePage/index.track.test.js @@ -20,13 +20,12 @@ jest.mock('../../utils/tracking'); import _throttle from 'lodash/throttle'; import { - FILTER_CLEAR, - FILTER_SET, - filterContext, - getRangeCmd, - RANGE_REFRAME, - RANGE_SHIFT, - rangeContext, + ACTION_FILTER_CLEAR, + ACTION_FILTER_SET, + ACTION_RANGE_REFRAME, + ACTION_RANGE_SHIFT, + CATEGORY_FILTER, + CATEGORY_RANGE, trackFilter, trackRange, } from './index.track'; @@ -41,8 +40,8 @@ describe('trackFilter', () => { const calls = _throttle.mock.calls; expect(calls.length).toBe(2); expect(calls).toEqual([ - [jasmine.any(Function), 750, { leading: false }], - [jasmine.any(Function), 750, { leading: false }], + [expect.any(Function), 750, { leading: false }], + [expect.any(Function), 750, { leading: false }], ]); }); @@ -50,102 +49,89 @@ describe('trackFilter', () => { expect(trackEvent.mock.calls.length).toBe(0); trackFilter('abc'); expect(trackEvent.mock.calls.length).toBe(1); - expect(trackEvent.mock.calls[0]).toEqual([ - { - category: filterContext, - action: FILTER_SET, - }, - ]); + expect(trackEvent.mock.calls[0]).toEqual([CATEGORY_FILTER, ACTION_FILTER_SET]); }); it('tracks filter clear when clearing the value', () => { expect(trackEvent.mock.calls.length).toBe(0); trackFilter(); expect(trackEvent.mock.calls.length).toBe(1); - expect(trackEvent.mock.calls[0]).toEqual([ - { - category: filterContext, - action: FILTER_CLEAR, - }, - ]); + expect(trackEvent.mock.calls[0]).toEqual([CATEGORY_FILTER, ACTION_FILTER_CLEAR]); }); }); -describe('getRangeCmd', () => { +describe('trackRange', () => { + beforeEach(() => { + trackEvent.mockClear(); + }); + const cases = [ { msg: 'returns shift if start is unchanged', - rangeType: RANGE_SHIFT, - args: [[0, 0.5], [0, 0.6]], + rangeType: ACTION_RANGE_SHIFT, + source: `${Math.random()}`, + from: [0, 0.5], + to: [0, 0.6], }, { msg: 'returns shift if end is unchanged', - rangeType: RANGE_SHIFT, - args: [[0, 0.5], [0.1, 0.5]], + rangeType: ACTION_RANGE_SHIFT, + source: `${Math.random()}`, + from: [0, 0.5], + to: [0.1, 0.5], }, { msg: 'returns shift if increasing start and end by same amount', - rangeType: RANGE_SHIFT, - args: [[0.25, 0.75], [0.5, 1]], + rangeType: ACTION_RANGE_SHIFT, + source: `${Math.random()}`, + from: [0.25, 0.75], + to: [0.5, 1], }, { msg: 'returns shift if decreasing start and end by same amount', - rangeType: RANGE_SHIFT, - args: [[0.25, 0.75], [0, 0.5]], + rangeType: ACTION_RANGE_SHIFT, + source: `${Math.random()}`, + from: [0.25, 0.75], + to: [0, 0.5], }, { msg: 'returns reframe if increasing start and end by different amounts', - rangeType: RANGE_REFRAME, - args: [[0.25, 0.75], [0.35, 1]], + rangeType: ACTION_RANGE_REFRAME, + source: `${Math.random()}`, + from: [0.25, 0.75], + to: [0.35, 1], }, { msg: 'returns reframe if decreasing start and end by different amounts', - rangeType: RANGE_REFRAME, - args: [[0.25, 0.75], [0, 0.65]], + rangeType: ACTION_RANGE_REFRAME, + source: `${Math.random()}`, + from: [0.25, 0.75], + to: [0, 0.65], }, { msg: 'returns reframe when widening to a superset', - rangeType: RANGE_REFRAME, - args: [[0.25, 0.75], [0, 1]], + rangeType: ACTION_RANGE_REFRAME, + source: `${Math.random()}`, + from: [0.25, 0.75], + to: [0, 1], }, { msg: 'returns reframe when contracting to a subset', - rangeType: RANGE_REFRAME, - args: [[0.25, 0.75], [0.45, 0.55]], + rangeType: ACTION_RANGE_REFRAME, + source: `${Math.random()}`, + from: [0.25, 0.75], + to: [0.45, 0.55], }, ]; cases.forEach(_case => { - const { msg, rangeType, args } = _case; + const { msg, rangeType, source, from, to } = _case; + it(msg, () => { - const rv = getRangeCmd(...args); - expect(rv).toBe(rangeType); + expect(trackEvent.mock.calls.length).toBe(0); + trackRange(source, from, to); + expect(trackEvent.mock.calls.length).toBe(1); + expect(trackEvent.mock.calls[0]).toEqual([CATEGORY_RANGE, rangeType, source]); }); }); }); - -describe('trackRange', () => { - beforeEach(() => { - trackEvent.mockClear(); - }); - - it('does nothing if given an invalid command', () => { - expect(trackEvent.mock.calls.length).toBe(0); - trackRange('invalid-command', 'abc'); - expect(trackEvent.mock.calls.length).toBe(0); - }); - - it('tracks the given context, command and source', () => { - const source = 'some-source'; - expect(trackEvent.mock.calls.length).toBe(0); - trackRange(RANGE_SHIFT, source); - expect(trackEvent.mock.calls.length).toBe(1); - expect(trackEvent.mock.calls[0]).toEqual([ - { - category: rangeContext, - action: RANGE_SHIFT, - label: source, - }, - ]); - }); -}); diff --git a/src/middlewares/track.js b/src/middlewares/track.js index 9aee61e4e4..5d3ce67f46 100644 --- a/src/middlewares/track.js +++ b/src/middlewares/track.js @@ -14,30 +14,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { middlewareHooks as spanDetails } from '../components/TracePage/TraceTimelineViewer/SpanDetail/index.track'; -import { middlewareHooks as spanBarRow } from '../components/TracePage/TraceTimelineViewer/SpanBarRow.track'; -import { middlewareHooks as spanDetailRow } from '../components/TracePage/TraceTimelineViewer/SpanDetailRow.track'; -import { middlewareHooks as headerRow } from '../components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineHeaderRow.track'; +import { middlewareHooks } from '../components/TracePage/TraceTimelineViewer/duck.track'; import { isGaEnabled } from '../utils/tracking'; -const trackFns = { ...spanDetails, ...spanBarRow, ...spanDetailRow, ...headerRow }; - -const keysCount = [spanDetails, spanBarRow, spanDetailRow, headerRow].reduce( - (total, middleware) => total + Object.keys(middleware).length, - 0 -); - -if (Object.keys(trackFns).length !== keysCount) { - // eslint-disable-next-line no-console - console.warn('a redux action type has more than one matching tracker middleware'); -} - function trackingMiddleware(store: { getState: () => any }) { return function inner(next: any => void) { return function core(action: any) { const { type } = action; - if (typeof trackFns[type] === 'function') { - trackFns[type](store, action); + if (typeof middlewareHooks[type] === 'function') { + middlewareHooks[type](store, action); } return next(action); }; diff --git a/src/utils/get-last-xform-cacher.test.js b/src/utils/get-last-xform-cacher.test.js index 3efdeb383e..8ab0245df4 100644 --- a/src/utils/get-last-xform-cacher.test.js +++ b/src/utils/get-last-xform-cacher.test.js @@ -24,7 +24,7 @@ beforeEach(() => { }); it('returns a function', () => { - expect(cacher).toEqual(jasmine.any(Function)); + expect(cacher).toEqual(expect.any(Function)); }); it('handles the first invocation where nothing is cached', () => { diff --git a/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineHeaderRow.track.js b/src/utils/tracking/common.js similarity index 58% rename from src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineHeaderRow.track.js rename to src/utils/tracking/common.js index 81071facf4..031988629c 100644 --- a/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineHeaderRow.track.js +++ b/src/utils/tracking/common.js @@ -14,20 +14,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { actionTypes as types } from '../duck'; -import { trackEvent } from '../../../../utils/tracking'; +export const OPEN = 'open'; +export const CLOSE = 'close'; -const cmd = 'resize'; -const context = 'jaeger/ux/trace/timeline/column'; - -export default function track(value: number) { - trackEvent({ - category: context, - action: cmd, - value: Math.round(value * 1000), - }); +export function getToggleValue(value: any) { + return value ? CLOSE : OPEN; } - -export const middlewareHooks = { - [types.SET_SPAN_NAME_COLUMN_WIDTH]: (_, action) => track(action.payload.width), -}; diff --git a/src/utils/tracking/get-event-tracker.js b/src/utils/tracking/get-event-tracker.js new file mode 100644 index 0000000000..2043114e5f --- /dev/null +++ b/src/utils/tracking/get-event-tracker.js @@ -0,0 +1,46 @@ +// @flow + +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { trackEvent } from './index'; + +type Stringable = string | ((...any) => string); +type Numberable = number | ((...any) => number); + +export default function getEventTracker( + categoryArg: Stringable, + actionArg: Stringable = String, + labelOrValueArg?: Stringable | Numberable, + valueArg?: Numberable +) { + const numArgs = arguments.length; + if (numArgs < 2) { + // eslint-disable-next-line no-console + throw new Error('Invalid event tracker, not enough arguments'); + } + return function track(...trackArgs: any[]) { + const eventData = [ + typeof categoryArg === 'function' ? categoryArg(...trackArgs) : categoryArg, + typeof actionArg === 'function' ? actionArg(...trackArgs) : actionArg, + ]; + if (numArgs >= 3) { + eventData.push(typeof labelOrValueArg === 'function' ? labelOrValueArg(...trackArgs) : labelOrValueArg); + } + if (numArgs >= 4) { + eventData.push(typeof valueArg === 'function' ? valueArg(...trackArgs) : valueArg); + } + trackEvent(...eventData); + }; +} diff --git a/src/utils/tracking/index.js b/src/utils/tracking/index.js index f9b7dabc69..c52ee37721 100644 --- a/src/utils/tracking/index.js +++ b/src/utils/tracking/index.js @@ -22,13 +22,6 @@ import Raven from 'raven-js'; import convRavenToGa from './conv-raven-to-ga'; import getConfig from '../config/get-config'; -type EventData = { - category: string, - action?: string, - label?: string, - value?: number, -}; - const EVENT_LENGTHS = { action: 499, category: 149, @@ -87,24 +80,30 @@ export function trackError(description: string) { } } -export function trackEvent(data: EventData) { +export function trackEvent( + category: string, + action: string, + labelOrValue?: ?string | ?number, + value?: ?number +) { if (isGaEnabled) { const event = {}; - let category = data.category; - if (!category) { - category = 'jaeger/event'; - } else if (!/^jaeger/i.test(category)) { - category = `jaeger/${category}`.slice(0, EVENT_LENGTHS.category); + if (!/^jaeger/i.test(category)) { + event.category = `jaeger/${category}`.slice(0, EVENT_LENGTHS.category); } else { - category = category.slice(0, EVENT_LENGTHS.category); + event.category = category.slice(0, EVENT_LENGTHS.category); } - event.category = category; - event.action = data.action ? data.action.slice(0, EVENT_LENGTHS.action) : 'jaeger/action'; - if (data.label) { - event.label = data.label.slice(0, EVENT_LENGTHS.label); + event.action = action.slice(0, EVENT_LENGTHS.action); + if (labelOrValue != null) { + if (typeof labelOrValue === 'string') { + event.label = labelOrValue.slice(0, EVENT_LENGTHS.action); + } else { + // value should be an int + event.value = Math.round(labelOrValue); + } } - if (data.value != null) { - event.value = Number(data.value); + if (value != null) { + event.value = Math.round(value); } ReactGA.event(event); if (isDebugMode) { @@ -114,25 +113,9 @@ export function trackEvent(data: EventData) { } function trackRavenError(ravenData: RavenTransportOptions) { - const data = convRavenToGa(ravenData); - if (isDebugMode) { - /* istanbul ignore next */ - Object.keys(data).forEach(key => { - if (key === 'message') { - return; - } - let valueLen = ''; - if (typeof data[key] === 'string') { - valueLen = `- value length: ${data[key].length}`; - } - // eslint-disable-next-line no-console - console.log(key, valueLen); - // eslint-disable-next-line no-console - console.log(data[key]); - }); - } - trackError(data.message); - trackEvent(data); + const { message, category, action, label, value } = convRavenToGa(ravenData); + trackError(message); + trackEvent(category, action, label, value); } // Tracking needs to be initialized when this file is imported, e.g. early in diff --git a/src/utils/tracking/index.test.js b/src/utils/tracking/index.test.js index 3f4c59896d..1ca9cdeb79 100644 --- a/src/utils/tracking/index.test.js +++ b/src/utils/tracking/index.test.js @@ -13,7 +13,11 @@ // limitations under the License. /* eslint-disable import/first */ -jest.mock('./conv-raven-to-ga', () => () => ({ message: 'jaeger/a' })); +jest.mock('./conv-raven-to-ga', () => () => ({ + category: 'jaeger/a', + action: 'some-action', + message: 'jaeger/a', +})); jest.mock('./index', () => { process.env.REACT_APP_VSN_STATE = '{}'; @@ -56,7 +60,7 @@ describe('tracking', () => { it('tracks an error', () => { tracking.trackError('a'); expect(calls).toEqual([ - ['send', { hitType: 'exception', exDescription: jasmine.any(String), exFatal: false }], + ['send', { hitType: 'exception', exDescription: expect.any(String), exFatal: false }], ]); }); @@ -76,30 +80,33 @@ describe('tracking', () => { describe('trackEvent', () => { it('tracks an event', () => { - tracking.trackEvent({ value: 10 }); + const category = 'jaeger/some-category'; + const action = 'some-action'; + tracking.trackEvent(category, action); expect(calls).toEqual([ [ 'send', { hitType: 'event', - eventCategory: jasmine.any(String), - eventAction: jasmine.any(String), - eventValue: 10, + eventCategory: category, + eventAction: action, }, ], ]); }); it('prepends "jaeger/" to the category, if needed', () => { - tracking.trackEvent({ category: 'a' }); + const category = 'some-category'; + const action = 'some-action'; + tracking.trackEvent(category, action); expect(calls).toEqual([ - ['send', { hitType: 'event', eventCategory: 'jaeger/a', eventAction: jasmine.any(String) }], + ['send', { hitType: 'event', eventCategory: `jaeger/${category}`, eventAction: action }], ]); }); it('truncates values, if needed', () => { const str = `jaeger/${getStr(600)}`; - tracking.trackEvent({ category: str, action: str, label: str }); + tracking.trackEvent(str, str, str); expect(calls).toEqual([ [ 'send', @@ -117,8 +124,8 @@ describe('tracking', () => { it('converting raven-js errors', () => { window.onunhandledrejection({ reason: new Error('abc') }); expect(calls).toEqual([ - ['send', { hitType: 'exception', exDescription: jasmine.any(String), exFatal: false }], - ['send', { hitType: 'event', eventCategory: jasmine.any(String), eventAction: jasmine.any(String) }], + ['send', { hitType: 'exception', exDescription: expect.any(String), exFatal: false }], + ['send', { hitType: 'event', eventCategory: expect.any(String), eventAction: expect.any(String) }], ]); }); }); From c60b135f7d17fce784ae29e3d3575d298600bd9a Mon Sep 17 00:00:00 2001 From: Joe Farro Date: Fri, 23 Mar 2018 15:47:15 -0400 Subject: [PATCH 17/18] Revert the "more concise" changes Signed-off-by: Joe Farro --- .../TracePage/KeyboardShortcutsHelp.track.js | 4 +- .../TracePage/TracePageHeader.track.js | 8 +-- .../TraceTimelineViewer/duck.track.js | 52 ++++++++----------- src/components/TracePage/index.track.js | 13 +++-- src/utils/tracking/get-event-tracker.js | 46 ---------------- 5 files changed, 36 insertions(+), 87 deletions(-) delete mode 100644 src/utils/tracking/get-event-tracker.js diff --git a/src/components/TracePage/KeyboardShortcutsHelp.track.js b/src/components/TracePage/KeyboardShortcutsHelp.track.js index 7472d04a38..4655d0c9df 100644 --- a/src/components/TracePage/KeyboardShortcutsHelp.track.js +++ b/src/components/TracePage/KeyboardShortcutsHelp.track.js @@ -15,8 +15,8 @@ // limitations under the License. import { OPEN } from '../../utils/tracking/common'; -import getEventTracker from '../../utils/tracking/get-event-tracker'; +import { trackEvent } from '../../utils/tracking'; const CATEGORY = 'jaeger/ux/trace/kbd-modal'; -export default getEventTracker(CATEGORY, OPEN); +export default trackEvent.bind(null, CATEGORY, OPEN); diff --git a/src/components/TracePage/TracePageHeader.track.js b/src/components/TracePage/TracePageHeader.track.js index 016c3308ab..023b18f749 100644 --- a/src/components/TracePage/TracePageHeader.track.js +++ b/src/components/TracePage/TracePageHeader.track.js @@ -15,11 +15,13 @@ // limitations under the License. import { getToggleValue, OPEN } from '../../utils/tracking/common'; -import getEventTracker from '../../utils/tracking/get-event-tracker'; +import { trackEvent } from '../../utils/tracking'; const CATEGORY_ALT_VIEW = 'jaeger/ux/trace/alt-view'; const CATEGORY_SLIM_HEADER = 'jaeger/ux/trace/slim-header'; -export const trackAltViewOpen = getEventTracker(CATEGORY_ALT_VIEW, OPEN); +// use a closure instead of bind to prevent forwarding any arguments to trackEvent() +export const trackAltViewOpen = () => trackEvent(CATEGORY_ALT_VIEW, OPEN); -export const trackSlimHeaderToggle = getEventTracker(CATEGORY_SLIM_HEADER, getToggleValue); +export const trackSlimHeaderToggle = (isOpen: boolean) => + trackEvent(CATEGORY_SLIM_HEADER, getToggleValue(isOpen)); diff --git a/src/components/TracePage/TraceTimelineViewer/duck.track.js b/src/components/TracePage/TraceTimelineViewer/duck.track.js index 7e275da57a..77fa53d616 100644 --- a/src/components/TracePage/TraceTimelineViewer/duck.track.js +++ b/src/components/TracePage/TraceTimelineViewer/duck.track.js @@ -14,14 +14,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -import _get from 'lodash/get'; - import type { Store } from 'redux'; import { actionTypes as types } from './duck'; import { trackEvent } from '../../../utils/tracking'; import { getToggleValue } from '../../../utils/tracking/common'; -import getEventTracker from '../../../utils/tracking/get-event-tracker'; const ACTION_RESIZE = 'resize'; @@ -32,27 +29,9 @@ export const CATEGORY_PROCESS = `${CATEGORY_BASE}/process`; export const CATEGORY_LOGS = `${CATEGORY_BASE}/logs`; export const CATEGORY_LOGS_ITEM = `${CATEGORY_BASE}/logs/item`; export const CATEGORY_COLUMN = `${CATEGORY_BASE}/column`; -export const CATEGORY_PARENT = `${CATEGORY_BASE}parent`; +export const CATEGORY_PARENT = `${CATEGORY_BASE}/parent`; export const CATEGORY_ROW = `${CATEGORY_BASE}/row`; -const isLogItemOpen = (openedItems, reduxAction) => openedItems.has(reduxAction.payload.logItem); - -function getDetailStateConverter(detailPath, transformer: (...any) => boolean = Boolean) { - return function getAction(store, reduxAction) { - const { spanID } = reduxAction.payload; - const detail = store.getState().traceTimeline.detailStates.get(spanID); - const detailItem = _get(detail, detailPath); - return getToggleValue(transformer(detailItem, reduxAction)); - }; -} - -const getColumnWidth = (_, action): number => Math.round(action.payload.width * 1000); - -function getDetailRowToggleAction(store, action) { - const detailIsOpen = store.getState().traceTimeline.detailStates.has(action.payload.spanID); - return getToggleValue(detailIsOpen); -} - function trackParent(store: Store, action: any) { const st = store.getState(); const { spanID } = action.payload; @@ -64,15 +43,26 @@ function trackParent(store: Store, action: any) { } } +const logs = (isOpen: boolean) => trackEvent(CATEGORY_LOGS, getToggleValue(isOpen)); +const logsItem = (isOpen: boolean) => trackEvent(CATEGORY_LOGS_ITEM, getToggleValue(isOpen)); +const process = (isOpen: boolean) => trackEvent(CATEGORY_PROCESS, getToggleValue(isOpen)); +const tags = (isOpen: boolean) => trackEvent(CATEGORY_TAGS, getToggleValue(isOpen)); +const detailRow = (isOpen: boolean) => trackEvent(CATEGORY_ROW, getToggleValue(isOpen)); +const columnWidth = (_, action) => + trackEvent(CATEGORY_COLUMN, ACTION_RESIZE, Math.round(action.payload.width * 1000)); + +const getDetail = (store, action) => store.getState().traceTimeline.detailStates.get(action.payload.spanID); + export const middlewareHooks = { [types.CHILDREN_TOGGLE]: trackParent, - [types.DETAIL_TOGGLE]: getEventTracker(CATEGORY_ROW, getDetailRowToggleAction), - [types.DETAIL_TAGS_TOGGLE]: getEventTracker(CATEGORY_TAGS, getDetailStateConverter('isTagsOpen')), - [types.DETAIL_PROCESS_TOGGLE]: getEventTracker(CATEGORY_PROCESS, getDetailStateConverter('isProcessOpen')), - [types.DETAIL_LOGS_TOGGLE]: getEventTracker(CATEGORY_LOGS, getDetailStateConverter('logs.isOpen')), - [types.DETAIL_LOG_ITEM_TOGGLE]: getEventTracker( - CATEGORY_LOGS_ITEM, - getDetailStateConverter('logs.openedItems', isLogItemOpen) - ), - [types.SET_SPAN_NAME_COLUMN_WIDTH]: getEventTracker(CATEGORY_COLUMN, ACTION_RESIZE, getColumnWidth), + [types.DETAIL_TOGGLE]: (store, action) => detailRow(Boolean(getDetail(store, action))), + [types.DETAIL_TAGS_TOGGLE]: (store, action) => tags(getDetail(store, action).isTagsOpen), + [types.DETAIL_PROCESS_TOGGLE]: (store, action) => process(getDetail(store, action).isProcessOpen), + [types.DETAIL_LOGS_TOGGLE]: (store, action) => logs(getDetail(store, action).logs.isOpen), + [types.DETAIL_LOG_ITEM_TOGGLE]: (store, action) => { + const detail = getDetail(store, action); + const { logItem } = action.payload; + logsItem(detail.logs.openedItems.has(logItem)); + }, + [types.SET_SPAN_NAME_COLUMN_WIDTH]: columnWidth, }; diff --git a/src/components/TracePage/index.track.js b/src/components/TracePage/index.track.js index 84d0313063..12bc98c0e1 100644 --- a/src/components/TracePage/index.track.js +++ b/src/components/TracePage/index.track.js @@ -16,7 +16,7 @@ import _throttle from 'lodash/throttle'; -import getEventTracker from '../../utils/tracking/get-event-tracker'; +import { trackEvent } from '../../utils/tracking'; // export for tests export const CATEGORY_RANGE = 'jaeger/ux/trace/range'; @@ -28,17 +28,17 @@ export const ACTION_FILTER_CLEAR = 'clear'; export const ACTION_RANGE_REFRAME = 'reframe'; export const ACTION_RANGE_SHIFT = 'shift'; -const trackFilterSet = _throttle(getEventTracker(CATEGORY_FILTER, ACTION_FILTER_SET), 750, { +const trackFilterSet = _throttle(trackEvent.bind(null, CATEGORY_FILTER, ACTION_FILTER_SET), 750, { leading: false, }); -const trackFilterClear = _throttle(getEventTracker(CATEGORY_FILTER, ACTION_FILTER_CLEAR), 750, { +const trackFilterClear = _throttle(trackEvent.bind(null, CATEGORY_FILTER, ACTION_FILTER_CLEAR), 750, { leading: false, }); export const trackFilter = (value: any) => (value ? trackFilterSet() : trackFilterClear()); -function getRangeAction(_: string, current: [number, number], next: [number, number]) { +function getRangeAction(current: [number, number], next: [number, number]) { const [curStart, curEnd] = current; const [nxStart, nxEnd] = next; if (curStart === nxStart || curEnd === nxEnd) { @@ -52,4 +52,7 @@ function getRangeAction(_: string, current: [number, number], next: [number, num return ACTION_RANGE_REFRAME; } -export const trackRange = getEventTracker(CATEGORY_RANGE, getRangeAction, String); +export function trackRange(source: string, current: [number, number], next: [number, number]) { + const action = getRangeAction(current, next); + trackEvent(CATEGORY_RANGE, action, source); +} diff --git a/src/utils/tracking/get-event-tracker.js b/src/utils/tracking/get-event-tracker.js deleted file mode 100644 index 2043114e5f..0000000000 --- a/src/utils/tracking/get-event-tracker.js +++ /dev/null @@ -1,46 +0,0 @@ -// @flow - -// Copyright (c) 2017 Uber Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { trackEvent } from './index'; - -type Stringable = string | ((...any) => string); -type Numberable = number | ((...any) => number); - -export default function getEventTracker( - categoryArg: Stringable, - actionArg: Stringable = String, - labelOrValueArg?: Stringable | Numberable, - valueArg?: Numberable -) { - const numArgs = arguments.length; - if (numArgs < 2) { - // eslint-disable-next-line no-console - throw new Error('Invalid event tracker, not enough arguments'); - } - return function track(...trackArgs: any[]) { - const eventData = [ - typeof categoryArg === 'function' ? categoryArg(...trackArgs) : categoryArg, - typeof actionArg === 'function' ? actionArg(...trackArgs) : actionArg, - ]; - if (numArgs >= 3) { - eventData.push(typeof labelOrValueArg === 'function' ? labelOrValueArg(...trackArgs) : labelOrValueArg); - } - if (numArgs >= 4) { - eventData.push(typeof valueArg === 'function' ? valueArg(...trackArgs) : valueArg); - } - trackEvent(...eventData); - }; -} From c9673fdc9c6e1437d300aed4bd7eaf05a7e8dc94 Mon Sep 17 00:00:00 2001 From: Joe Farro Date: Mon, 26 Mar 2018 14:02:28 -0400 Subject: [PATCH 18/18] Update changelog Signed-off-by: Joe Farro --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d959aa0f59..b11ae5473f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changes merged into master +### [#191](https://github.com/jaegertracing/jaeger-ui/pull/191) Add GA event tracking for actions in trace view + +* Partially addresses [#157](https://github.com/jaegertracing/jaeger-ui/issues/157) - Enhanced Google Analytics integration + +### [#198](https://github.com/jaegertracing/jaeger-ui/pull/198) Use `` and config webpack at runtime to allow path prefix + +* Fix [#42](https://github.com/jaegertracing/jaeger-ui/issues/42) - No support for Jaeger behind a reverse proxy + +### [#195](https://github.com/jaegertracing/jaeger-ui/pull/195) Handle Error stored in redux trace.traces + +* Fix [#166](https://github.com/jaegertracing/jaeger-ui/issues/166) - JS error on search page after viewing 404 trace + ### [#192](https://github.com/jaegertracing/jaeger-ui/pull/192) Change fallback trace name to be more informative * Fix [#190](https://github.com/jaegertracing/jaeger-ui/issues/190) - Change `cannot-find-trace-name` to `trace-without-root-span`