Skip to content

Commit

Permalink
Block requests to ui_metric API if telemetry is disabled (elastic#35268)
Browse files Browse the repository at this point in the history
* Block requests to ui_metric API if telemetry is disabled
- Export trackUiMetric helper method from ui_metric app.
- Remove createUiMetricUri helper.
- Add support for an array of metric types.
- Update README.
* Throw error if trackUiMetric is called with a string containing a colon.
  • Loading branch information
cjcenizal authored and Liza Katz committed Apr 29, 2019
1 parent 73c5e03 commit fe8e764
Show file tree
Hide file tree
Showing 23 changed files with 181 additions and 60 deletions.
28 changes: 21 additions & 7 deletions src/legacy/core_plugins/ui_metric/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,29 @@ the name of a dashboard they've viewed, or the timestamp of the interaction.

## How to use it

To track a user interaction, simply send a `POST` request to `/api/ui_metric/{APP_NAME}/{METRIC_TYPE}`,
where `APP_NAME` and `METRIC_TYPE` are underscore-delimited strings. For example, to track the app
`my_app` and the metric `my_metric`, send a request to `/api/ui_metric/my_app/my_metric`.
To track a user interaction, import the `trackUiMetric` helper function from UI Metric app:

```js
import { trackUiMetric } from 'relative/path/to/src/legacy/core_plugins/ui_metric/public';
```

Call this function whenever you would like to track a user interaction within your app. The function
accepts two arguments, `appName` and `metricType`. These should be underscore-delimited strings.
For example, to track the `my_metric` metric in the app `my_app` call `trackUiMetric('my_app', 'my_metric)`.

That's all you need to do!

To track multiple metrics within a single request, provide multiple metric types separated by
commas, e.g. `/api/ui_metric/my_app/my_metric1,my_metric2,my_metric3`.
To track multiple metrics within a single request, provide an array of metric types, e.g. `trackUiMetric('my_app', ['my_metric1', 'my_metric2', 'my_metric3'])`.

**NOTE:** When called, this function sends a `POST` request to `/api/ui_metric/{appName}/{metricType}`.
It's important that this request is sent via the `trackUiMetric` function, because it contains special
logic for blocking the request if the user hasn't opted in to telemetry.

### Disallowed characters

The colon and comma characters (`,`, `:`) should not be used in app name or metric types. Colons play
a sepcial role in how metrics are stored as saved objects, and the API endpoint uses commas to delimit
multiple metric types in a single API request.

### Tracking timed interactions

Expand All @@ -32,8 +47,7 @@ logic yourself. You'll also need to predefine some buckets into which the UI met
For example, if you're timing how long it takes to create a visualization, you may decide to
measure interactions that take less than 1 minute, 1-5 minutes, 5-20 minutes, and longer than 20 minutes.
To track these interactions, you'd use the timed length of the interaction to determine whether to
hit `/api/ui_metric/visualize/create_vis_1m`, `/api/ui_metric/visualize/create_vis_5m`,
`/api/ui_metric/visualize/create_vis_20m`, etc.
use a `metricType` of `create_vis_1m`, `create_vis_5m`, `create_vis_20m`, or `create_vis_infinity`.

## How it works

Expand Down
20 changes: 20 additions & 0 deletions src/legacy/core_plugins/ui_metric/common/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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.
*/

export const API_BASE_PATH = '/api/ui_metric';
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,26 @@
* under the License.
*/

import { resolve } from 'path';
import { Legacy } from '../../../../kibana';
import { registerUserActionRoute } from './server/routes/api/ui_metric';
import { registerUiMetricUsageCollector } from './server/usage/index';

export default function (kibana) {
// eslint-disable-next-line import/no-default-export
export default function(kibana: any) {
return new kibana.Plugin({
id: 'ui_metric',
require: ['kibana', 'elasticsearch'],
publicDir: resolve(__dirname, 'public'),

uiExports: {
mappings: require('./mappings.json'),
hacks: ['plugins/ui_metric'],
},

init: function (server) {
init(server: Legacy.Server) {
registerUserActionRoute(server);
registerUiMetricUsageCollector(server);
}
},
});
}
55 changes: 55 additions & 0 deletions src/legacy/core_plugins/ui_metric/public/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 chrome from 'ui/chrome';
// @ts-ignore
import { uiModules } from 'ui/modules';
import { getCanTrackUiMetrics } from 'ui/ui_metric';
import { API_BASE_PATH } from '../common';

let _http: any;

uiModules.get('kibana').run(($http: any) => {
_http = $http;
});

function createErrorMessage(subject: string): any {
const message =
`trackUiMetric was called with ${subject}, which is not allowed to contain a colon. ` +
`Colons play a special role in how metrics are saved as stored objects`;
return new Error(message);
}

export function trackUiMetric(appName: string, metricType: string | string[]) {
if (!getCanTrackUiMetrics()) {
return;
}

if (appName.includes(':')) {
throw createErrorMessage(`app name '${appName}'`);
}

if (metricType.includes(':')) {
throw createErrorMessage(`metric type ${metricType}`);
}

const metricTypes = Array.isArray(metricType) ? metricType.join(',') : metricType;
const uri = chrome.addBasePath(`${API_BASE_PATH}/${appName}/${metricTypes}`);
_http.post(uri);
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,14 @@

import Boom from 'boom';
import { Server } from 'hapi';
import { API_BASE_PATH } from '../../../common';

export const registerUserActionRoute = (server: Server) => {
/*
* Increment a count on an object representing a specific interaction with the UI.
*/
server.route({
path: '/api/ui_metric/{appName}/{metricTypes}',
path: `${API_BASE_PATH}/{appName}/{metricTypes}`,
method: 'POST',
handler: async (request: any) => {
const { appName, metricTypes } = request.params;
Expand Down
28 changes: 28 additions & 0 deletions src/legacy/ui/public/ui_metric/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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.
*/

let _canTrackUiMetrics = false;

export function setCanTrackUiMetrics(flag: boolean) {
_canTrackUiMetrics = flag;
}

export function getCanTrackUiMetrics(): boolean {
return _canTrackUiMetrics;
}
7 changes: 0 additions & 7 deletions x-pack/common/ui_metric/index.ts

This file was deleted.

11 changes: 0 additions & 11 deletions x-pack/common/ui_metric/ui_metric.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ jest.mock('ui/index_patterns', () => {
return { INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE };
});

jest.mock('../../../../../src/legacy/core_plugins/ui_metric/public', () => ({
trackUiMetric: jest.fn(),
}));

const { setup } = pageHelpers.autoFollowPatternList;

describe('<AutoFollowPatternList />', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ jest.mock('ui/index_patterns', () => {
return { INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE };
});

jest.mock('../../../../../src/legacy/core_plugins/ui_metric/public', () => ({
trackUiMetric: jest.fn(),
}));

const { setup } = pageHelpers.followerIndexList;

describe('<FollowerIndicesList />', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ jest.mock('ui/index_patterns', () => {
return { INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE };
});

jest.mock('../../../../../src/legacy/core_plugins/ui_metric/public', () => ({
trackUiMetric: jest.fn(),
}));

const { setup } = pageHelpers.home;

describe('<CrossClusterReplicationHome />', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export const createFollowerIndex = (followerIndex) => {
uiMetrics.push(UIM_FOLLOWER_INDEX_USE_ADVANCED_OPTIONS);
}
const request = httpClient.post(`${apiPrefix}/follower_indices`, followerIndex);
return trackUserRequest(request, uiMetrics.join(',')).then(extractData);
return trackUserRequest(request, uiMetrics).then(extractData);
};

export const pauseFollowerIndex = (id) => {
Expand Down Expand Up @@ -138,7 +138,7 @@ export const updateFollowerIndex = (id, followerIndex) => {
uiMetrics.push(UIM_FOLLOWER_INDEX_USE_ADVANCED_OPTIONS);
}
const request = httpClient.put(`${apiPrefix}/follower_indices/${encodeURIComponent(id)}`, followerIndex);
return trackUserRequest(request, uiMetrics.join(',')).then(extractData);
return trackUserRequest(request, uiMetrics).then(extractData);
};

/* Stats */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { createUiMetricUri } from '../../../../../common/ui_metric';
import { trackUiMetric as track } from '../../../../../../src/legacy/core_plugins/ui_metric/public';
import { UIM_APP_NAME } from '../constants';
import { getHttpClient } from './api';

export function trackUiMetric(actionType) {
const uiMetricUri = createUiMetricUri(UIM_APP_NAME, actionType);
getHttpClient().post(uiMetricUri);
track(UIM_APP_NAME, actionType);
}

/**
Expand Down
10 changes: 5 additions & 5 deletions x-pack/plugins/index_lifecycle_management/public/services/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export async function loadPolicies(withIndices, httpClient = getHttpClient()) {
export async function deletePolicy(policyName, httpClient = getHttpClient()) {
const response = await httpClient.delete(`${apiPrefix}/policies/${encodeURIComponent(policyName)}`);
// Only track successful actions.
trackUiMetric(UIM_POLICY_DELETE, httpClient);
trackUiMetric(UIM_POLICY_DELETE);
return response.data;
}

Expand All @@ -73,27 +73,27 @@ export async function getAffectedIndices(indexTemplateName, policyName, httpClie
export const retryLifecycleForIndex = async (indexNames, httpClient = getHttpClient()) => {
const response = await httpClient.post(`${apiPrefix}/index/retry`, { indexNames });
// Only track successful actions.
trackUiMetric(UIM_INDEX_RETRY_STEP, httpClient);
trackUiMetric(UIM_INDEX_RETRY_STEP);
return response.data;
};

export const removeLifecycleForIndex = async (indexNames, httpClient = getHttpClient()) => {
const response = await httpClient.post(`${apiPrefix}/index/remove`, { indexNames });
// Only track successful actions.
trackUiMetric(UIM_POLICY_DETACH_INDEX, httpClient);
trackUiMetric(UIM_POLICY_DETACH_INDEX);
return response.data;
};

export const addLifecyclePolicyToIndex = async (body, httpClient = getHttpClient()) => {
const response = await httpClient.post(`${apiPrefix}/index/add`, body);
// Only track successful actions.
trackUiMetric(UIM_POLICY_ATTACH_INDEX, httpClient);
trackUiMetric(UIM_POLICY_ATTACH_INDEX);
return response.data;
};

export const addLifecyclePolicyToTemplate = async (body, httpClient = getHttpClient()) => {
const response = await httpClient.post(`${apiPrefix}/template`, body);
// Only track successful actions.
trackUiMetric(UIM_POLICY_ATTACH_INDEX_TEMPLATE, httpClient);
trackUiMetric(UIM_POLICY_ATTACH_INDEX_TEMPLATE);
return response.data;
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import { get } from 'lodash';

import { createUiMetricUri } from '../../../../common/ui_metric';
import { trackUiMetric as track } from '../../../../../src/legacy/core_plugins/ui_metric/public';

import {
UIM_APP_NAME,
Expand All @@ -29,11 +29,8 @@ import {
defaultHotPhase,
} from '../store/defaults';

import { getHttpClient } from './api';

export function trackUiMetric(metricType, httpClient = getHttpClient()) {
const uiMetricUri = createUiMetricUri(UIM_APP_NAME, metricType);
httpClient.post(uiMetricUri);
export function trackUiMetric(metricType) {
track(UIM_APP_NAME, metricType);
}

export function getUiMetricsForPhases(phases) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const saveLifecyclePolicy = (lifecycle, isNew) => async () => {

const uiMetrics = getUiMetricsForPhases(lifecycle.phases);
uiMetrics.push(isNew ? UIM_POLICY_CREATE : UIM_POLICY_UPDATE);
trackUiMetric(uiMetrics.join(','));
trackUiMetric(uiMetrics);

const message = i18n.translate('xpack.indexLifecycleMgmt.editPolicy.successfulSaveMessage',
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { createUiMetricUri } from '../../../../common/ui_metric';
import { trackUiMetric as track } from '../../../../../src/legacy/core_plugins/ui_metric/public';
import { UIM_APP_NAME } from '../../common/constants';
import { getHttpClient } from './api';

export function trackUiMetric(metricType) {
const uiMetricUri = createUiMetricUri(UIM_APP_NAME, metricType);
getHttpClient().post(uiMetricUri);
track(UIM_APP_NAME, metricType);
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ jest.mock('ui/chrome', () => ({
}
}));

jest.mock('../../../../../src/legacy/core_plugins/ui_metric/public', () => ({
trackUiMetric: jest.fn(),
}));

const { setup } = pageHelpers.remoteClustersList;

describe('<RemoteClusterList />', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { createUiMetricUri } from '../../../../common/ui_metric';
import { trackUiMetric as track } from '../../../../../src/legacy/core_plugins/ui_metric/public';
import { UIM_APP_NAME } from '../constants';
import { getHttpClient } from './api';

export function trackUiMetric(actionType) {
const uiMetricUri = createUiMetricUri(UIM_APP_NAME, actionType);
getHttpClient().post(uiMetricUri);
track(UIM_APP_NAME, actionType);
}

/**
Expand Down
Loading

0 comments on commit fe8e764

Please sign in to comment.