mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Instrument ILM with user action telemetry (#33089)
* Track user actions for creating, updating, and deleting and index lifecycle policy. * Track user actions for attaching an index, attaching an index template, and detaching an index. * Track app load action. * Track usage of cold phase, warm phase, set priority, and freeze index. * Track retry step user action. * Track clicks on the policy name to edit it. * Refactor constants to be in root of public. * Add support to user actions API for comma-delimited action types. * Refactor rollups trackUserRequest to be easier to understand. * Use underscores instead of dashes for user action app names. * Switch from componentWillMount to componentDidMount/useEffect. - Standardize use of HashRouter within app.js to avoid calling useEffect when leaving the app.
This commit is contained in:
parent
7676f03bca
commit
d8c7e18bf1
43 changed files with 400 additions and 89 deletions
|
@ -25,20 +25,23 @@ export const registerUserActionRoute = (server: Server) => {
|
|||
* Increment a count on an object representing a specific user action.
|
||||
*/
|
||||
server.route({
|
||||
path: '/api/user_action/{appName}/{actionType}',
|
||||
path: '/api/user_action/{appName}/{actionTypes}',
|
||||
method: 'POST',
|
||||
handler: async (request: any) => {
|
||||
const { appName, actionType } = request.params;
|
||||
const { appName, actionTypes } = request.params;
|
||||
|
||||
try {
|
||||
const { getSavedObjectsRepository } = server.savedObjects;
|
||||
const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin');
|
||||
const internalRepository = getSavedObjectsRepository(callWithInternalUser);
|
||||
const savedObjectId = `${appName}:${actionType}`;
|
||||
|
||||
// This object is created if it doesn't already exist.
|
||||
await internalRepository.incrementCounter('user-action', savedObjectId, 'count');
|
||||
const incrementRequests = actionTypes.split(',').map((actionType: string) => {
|
||||
const savedObjectId = `${appName}:${actionType}`;
|
||||
// This object is created if it doesn't already exist.
|
||||
return internalRepository.incrementCounter('user-action', savedObjectId, 'count');
|
||||
});
|
||||
|
||||
await Promise.all(incrementRequests);
|
||||
return {};
|
||||
} catch (error) {
|
||||
return new Boom('Something went wrong', { statusCode: error.status });
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { get } from 'lodash';
|
||||
|
||||
export default function ({ getService }) {
|
||||
const supertest = getService('supertest');
|
||||
|
@ -35,9 +34,24 @@ export default function ({ getService }) {
|
|||
index: '.kibana',
|
||||
q: 'type:user-action',
|
||||
}).then(response => {
|
||||
const doc = get(response, 'hits.hits[0]');
|
||||
expect(get(doc, '_source.user-action.count')).to.be(1);
|
||||
expect(doc._id).to.be('user-action:myApp:myAction');
|
||||
const ids = response.hits.hits.map(({ _id }) => _id);
|
||||
expect(ids.includes('user-action:myApp:myAction'));
|
||||
});
|
||||
});
|
||||
|
||||
it('supports comma-delimited action types', async () => {
|
||||
await supertest
|
||||
.post('/api/user_action/myApp/myAction1,myAction2')
|
||||
.set('kbn-xsrf', 'kibana')
|
||||
.expect(200);
|
||||
|
||||
return es.search({
|
||||
index: '.kibana',
|
||||
q: 'type:user-action',
|
||||
}).then(response => {
|
||||
const ids = response.hits.hits.map(({ _id }) => _id);
|
||||
expect(ids.includes('user-action:myApp:myAction1'));
|
||||
expect(ids.includes('user-action:myApp:myAction2'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,3 +6,20 @@
|
|||
|
||||
export const BASE_PATH = '/management/elasticsearch/index_lifecycle_management/';
|
||||
export const PLUGIN_ID = 'index_lifecycle_management';
|
||||
export {
|
||||
UA_APP_NAME,
|
||||
USER_ACTIONS,
|
||||
UA_APP_LOAD,
|
||||
UA_POLICY_CREATE,
|
||||
UA_POLICY_UPDATE,
|
||||
UA_POLICY_DELETE,
|
||||
UA_POLICY_ATTACH_INDEX,
|
||||
UA_POLICY_ATTACH_INDEX_TEMPLATE,
|
||||
UA_POLICY_DETACH_INDEX,
|
||||
UA_CONFIG_COLD_PHASE,
|
||||
UA_CONFIG_WARM_PHASE,
|
||||
UA_CONFIG_SET_PRIORITY,
|
||||
UA_CONFIG_FREEZE_INDEX,
|
||||
UA_INDEX_RETRY_STEP,
|
||||
UA_EDIT_CLICK,
|
||||
} from './user_action';
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const UA_APP_NAME = 'index_lifecycle_management';
|
||||
|
||||
export const UA_APP_LOAD = 'app_load';
|
||||
export const UA_POLICY_CREATE = 'policy_create';
|
||||
export const UA_POLICY_UPDATE = 'policy_update';
|
||||
export const UA_POLICY_DELETE = 'policy_delete';
|
||||
export const UA_POLICY_ATTACH_INDEX = 'policy_attach_index';
|
||||
export const UA_POLICY_ATTACH_INDEX_TEMPLATE = 'policy_attach_index_template';
|
||||
export const UA_POLICY_DETACH_INDEX = 'policy_detach_index';
|
||||
export const UA_CONFIG_COLD_PHASE = 'config_cold_phase';
|
||||
export const UA_CONFIG_WARM_PHASE = 'config_warm_phase';
|
||||
export const UA_CONFIG_SET_PRIORITY = 'config_set_priority';
|
||||
export const UA_CONFIG_FREEZE_INDEX = 'config_freeze_index';
|
||||
export const UA_INDEX_RETRY_STEP = 'index_retry_step';
|
||||
export const UA_EDIT_CLICK = 'edit_click';
|
||||
|
||||
export const USER_ACTIONS = [
|
||||
UA_APP_LOAD,
|
||||
UA_POLICY_CREATE,
|
||||
UA_POLICY_UPDATE,
|
||||
UA_POLICY_DELETE,
|
||||
UA_POLICY_ATTACH_INDEX,
|
||||
UA_POLICY_ATTACH_INDEX_TEMPLATE,
|
||||
UA_POLICY_DETACH_INDEX,
|
||||
UA_CONFIG_COLD_PHASE,
|
||||
UA_CONFIG_WARM_PHASE,
|
||||
UA_CONFIG_SET_PRIORITY,
|
||||
UA_CONFIG_FREEZE_INDEX,
|
||||
UA_INDEX_RETRY_STEP,
|
||||
UA_EDIT_CLICK,
|
||||
];
|
|
@ -13,6 +13,8 @@ import { registerIndexRoutes } from './server/routes/api/index';
|
|||
import { registerLicenseChecker } from './server/lib/register_license_checker';
|
||||
import { PLUGIN_ID } from './common/constants';
|
||||
import { indexLifecycleDataEnricher } from './index_lifecycle_data';
|
||||
import { registerIndexLifecycleManagementUsageCollector } from './server/usage';
|
||||
|
||||
export function indexLifecycleManagement(kibana) {
|
||||
return new kibana.Plugin({
|
||||
config: (Joi) => {
|
||||
|
@ -52,6 +54,8 @@ export function indexLifecycleManagement(kibana) {
|
|||
registerPoliciesRoutes(server);
|
||||
registerLifecycleRoutes(server);
|
||||
registerIndexRoutes(server);
|
||||
registerIndexLifecycleManagementUsageCollector(server);
|
||||
|
||||
if (
|
||||
server.config().get('xpack.ilm.ui.enabled') &&
|
||||
server.plugins.index_management &&
|
||||
|
|
|
@ -4,18 +4,23 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { HashRouter, Switch, Route, Redirect } from 'react-router-dom';
|
||||
import { BASE_PATH, UA_APP_LOAD } from '../common/constants';
|
||||
import { EditPolicy } from './sections/edit_policy';
|
||||
import { PolicyTable } from './sections/policy_table';
|
||||
import { BASE_PATH } from '../common/constants';
|
||||
import { trackUserAction } from './services';
|
||||
|
||||
export const App = () => (
|
||||
<HashRouter>
|
||||
<Switch>
|
||||
<Redirect exact from={`${BASE_PATH}`} to={`${BASE_PATH}policies`}/>
|
||||
<Route exact path={`${BASE_PATH}policies`} component={PolicyTable}/>
|
||||
<Route path={`${BASE_PATH}policies/edit/:policyName?`} component={EditPolicy}/>
|
||||
</Switch>
|
||||
</HashRouter>
|
||||
);
|
||||
export const App = () => {
|
||||
useEffect(() => trackUserAction(UA_APP_LOAD), []);
|
||||
|
||||
return (
|
||||
<HashRouter>
|
||||
<Switch>
|
||||
<Redirect exact from={`${BASE_PATH}`} to={`${BASE_PATH}policies`}/>
|
||||
<Route exact path={`${BASE_PATH}policies`} component={PolicyTable}/>
|
||||
<Route path={`${BASE_PATH}policies/edit/:policyName?`} component={EditPolicy}/>
|
||||
</Switch>
|
||||
</HashRouter>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
PHASE_COLD,
|
||||
PHASE_HOT,
|
||||
PHASE_ROLLOVER_ENABLED
|
||||
} from '../../../../store/constants';
|
||||
} from '../../../../constants';
|
||||
|
||||
export const ColdPhase = connect(
|
||||
(state) => ({
|
||||
|
|
|
@ -22,7 +22,7 @@ import {
|
|||
PHASE_ENABLED,
|
||||
PHASE_REPLICA_COUNT,
|
||||
PHASE_FREEZE_ENABLED
|
||||
} from '../../../../store/constants';
|
||||
} from '../../../../constants';
|
||||
import { ErrableFormRow } from '../../form_errors';
|
||||
import { MinAgeInput } from '../min_age_input';
|
||||
import { LearnMoreLink, ActiveBadge, PhaseErrorMessage, OptionalLabel } from '../../../components';
|
||||
|
|
|
@ -8,7 +8,7 @@ import { connect } from 'react-redux';
|
|||
import { DeletePhase as PresentationComponent } from './delete_phase';
|
||||
import { getPhase } from '../../../../store/selectors';
|
||||
import { setPhaseData } from '../../../../store/actions';
|
||||
import { PHASE_DELETE, PHASE_HOT, PHASE_ROLLOVER_ENABLED } from '../../../../store/constants';
|
||||
import { PHASE_DELETE, PHASE_HOT, PHASE_ROLLOVER_ENABLED } from '../../../../constants';
|
||||
|
||||
export const DeletePhase = connect(
|
||||
state => ({
|
||||
|
|
|
@ -19,7 +19,7 @@ import {
|
|||
import {
|
||||
PHASE_DELETE,
|
||||
PHASE_ENABLED,
|
||||
} from '../../../../store/constants';
|
||||
} from '../../../../constants';
|
||||
import { ActiveBadge, PhaseErrorMessage } from '../../../components';
|
||||
|
||||
export class DeletePhase extends PureComponent {
|
||||
|
|
|
@ -11,7 +11,7 @@ import { connect } from 'react-redux';
|
|||
import { HotPhase as PresentationComponent } from './hot_phase';
|
||||
import { getPhase } from '../../../../store/selectors';
|
||||
import { setPhaseData } from '../../../../store/actions';
|
||||
import { PHASE_HOT, PHASE_WARM, WARM_PHASE_ON_ROLLOVER } from '../../../../store/constants';
|
||||
import { PHASE_HOT, PHASE_WARM, WARM_PHASE_ON_ROLLOVER } from '../../../../constants';
|
||||
|
||||
export const HotPhase = connect(
|
||||
state => ({
|
||||
|
|
|
@ -27,7 +27,7 @@ import {
|
|||
PHASE_ROLLOVER_MAX_SIZE_STORED,
|
||||
PHASE_ROLLOVER_MAX_SIZE_STORED_UNITS,
|
||||
PHASE_ROLLOVER_ENABLED,
|
||||
} from '../../../../store/constants';
|
||||
} from '../../../../constants';
|
||||
import { SetPriorityInput } from '../set_priority_input';
|
||||
|
||||
import { ErrableFormRow } from '../../form_errors';
|
||||
|
|
|
@ -15,7 +15,7 @@ import {
|
|||
import {
|
||||
PHASE_ROLLOVER_MINIMUM_AGE,
|
||||
PHASE_ROLLOVER_MINIMUM_AGE_UNITS,
|
||||
} from '../../../store/constants';
|
||||
} from '../../../constants';
|
||||
import { ErrableFormRow } from '../form_errors';
|
||||
import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
|
||||
const MinAgeInputUi = props => {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import React, { Component, Fragment } from 'react';
|
||||
import { EuiSelect, EuiButtonEmpty, EuiCallOut, EuiSpacer, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
|
||||
import { PHASE_NODE_ATTRS } from '../../../../store/constants';
|
||||
import { PHASE_NODE_ATTRS } from '../../../../constants';
|
||||
import { ErrableFormRow } from '../../form_errors';
|
||||
import { LearnMoreLink } from '../../../components/learn_more_link';
|
||||
const learnMoreLinks = (
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import {
|
||||
PHASE_INDEX_PRIORITY,
|
||||
} from '../../../store/constants';
|
||||
} from '../../../constants';
|
||||
import { ErrableFormRow } from '../form_errors';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
export const SetPriorityInput = props => {
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
getPhase,
|
||||
} from '../../../../store/selectors';
|
||||
import { setPhaseData } from '../../../../store/actions';
|
||||
import { PHASE_WARM, PHASE_HOT, PHASE_ROLLOVER_ENABLED } from '../../../../store/constants';
|
||||
import { PHASE_WARM, PHASE_HOT, PHASE_ROLLOVER_ENABLED } from '../../../../constants';
|
||||
|
||||
export const WarmPhase = connect(
|
||||
state => ({
|
||||
|
|
|
@ -26,7 +26,7 @@ import {
|
|||
PHASE_PRIMARY_SHARD_COUNT,
|
||||
PHASE_REPLICA_COUNT,
|
||||
PHASE_SHRINK_ENABLED,
|
||||
} from '../../../../store/constants';
|
||||
} from '../../../../constants';
|
||||
import { SetPriorityInput } from '../set_priority_input';
|
||||
import { NodeAllocation } from '../node_allocation';
|
||||
import { ErrableFormRow } from '../../form_errors';
|
||||
|
|
|
@ -36,7 +36,7 @@ import {
|
|||
PHASE_DELETE,
|
||||
PHASE_WARM,
|
||||
STRUCTURE_POLICY_NAME,
|
||||
} from '../../store/constants';
|
||||
} from '../../constants';
|
||||
import { findFirstError } from '../../services/find_errors';
|
||||
import { NodeAttrsDetails } from './components/node_attrs_details';
|
||||
import { PolicyJsonFlyout } from './components/policy_json_flyout';
|
||||
|
|
|
@ -8,11 +8,7 @@ import React, { Component, Fragment } from 'react';
|
|||
import moment from 'moment-timezone';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
|
||||
import { BASE_PATH } from '../../../../../common/constants';
|
||||
import { NoMatch } from '../no_match';
|
||||
import { getPolicyPath } from '../../../../services/navigation';
|
||||
import { flattenPanelTree } from '../../../../services/flatten_panel_tree';
|
||||
import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services';
|
||||
|
||||
import {
|
||||
EuiBetaBadge,
|
||||
EuiButton,
|
||||
|
@ -38,10 +34,17 @@ import {
|
|||
EuiPageBody,
|
||||
EuiPageContent,
|
||||
} from '@elastic/eui';
|
||||
import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services';
|
||||
|
||||
import { getIndexListUri } from '../../../../../../index_management/public/services/navigation';
|
||||
import { BASE_PATH, UA_EDIT_CLICK } from '../../../../../common/constants';
|
||||
import { getPolicyPath } from '../../../../services/navigation';
|
||||
import { flattenPanelTree } from '../../../../services/flatten_panel_tree';
|
||||
import { trackUserAction } from '../../../../services';
|
||||
import { NoMatch } from '../no_match';
|
||||
import { ConfirmDelete } from './confirm_delete';
|
||||
import { AddPolicyToTemplateConfirmModal } from './add_policy_to_template_confirm_modal';
|
||||
import { getIndexListUri } from '../../../../../../index_management/public/services/navigation';
|
||||
|
||||
const COLUMNS = {
|
||||
name: {
|
||||
label: i18n.translate('xpack.indexLifecycleMgmt.policyTable.headers.nameHeader', {
|
||||
|
@ -176,6 +179,7 @@ export class PolicyTableUi extends Component {
|
|||
className="policyTable__link"
|
||||
data-test-subj="policyTablePolicyNameLink"
|
||||
href={getPolicyPath(value)}
|
||||
onClick={() => trackUserAction(UA_EDIT_CLICK)}
|
||||
>
|
||||
{value}
|
||||
</EuiLink>
|
||||
|
|
|
@ -4,12 +4,20 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import chrome from 'ui/chrome';
|
||||
import {
|
||||
UA_POLICY_DELETE,
|
||||
UA_POLICY_ATTACH_INDEX,
|
||||
UA_POLICY_ATTACH_INDEX_TEMPLATE,
|
||||
UA_POLICY_DETACH_INDEX,
|
||||
UA_INDEX_RETRY_STEP,
|
||||
} from '../../common/constants';
|
||||
import { trackUserAction } from './user_action';
|
||||
|
||||
let httpClient;
|
||||
export const setHttpClient = (client) => {
|
||||
httpClient = client;
|
||||
};
|
||||
const getHttpClient = () => {
|
||||
export const getHttpClient = () => {
|
||||
return httpClient;
|
||||
};
|
||||
const apiPrefix = chrome.addBasePath('/api/index_lifecycle_management');
|
||||
|
@ -44,6 +52,8 @@ 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.
|
||||
trackUserAction(UA_POLICY_DELETE, httpClient);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
|
@ -52,7 +62,6 @@ export async function saveLifecycle(lifecycle, httpClient = getHttpClient()) {
|
|||
return response.data;
|
||||
}
|
||||
|
||||
|
||||
export async function getAffectedIndices(indexTemplateName, policyName, httpClient = getHttpClient()) {
|
||||
const path = policyName
|
||||
? `${apiPrefix}/indices/affected/${indexTemplateName}/${encodeURIComponent(policyName)}`
|
||||
|
@ -60,19 +69,31 @@ export async function getAffectedIndices(indexTemplateName, policyName, httpClie
|
|||
const response = await httpClient.get(path);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export const retryLifecycleForIndex = async (indexNames, httpClient = getHttpClient()) => {
|
||||
const response = await httpClient.post(`${apiPrefix}/index/retry`, { indexNames });
|
||||
// Only track successful actions.
|
||||
trackUserAction(UA_INDEX_RETRY_STEP, httpClient);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const removeLifecycleForIndex = async (indexNames, httpClient = getHttpClient()) => {
|
||||
const response = await httpClient.post(`${apiPrefix}/index/remove`, { indexNames });
|
||||
// Only track successful actions.
|
||||
trackUserAction(UA_POLICY_DETACH_INDEX, httpClient);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const addLifecyclePolicyToIndex = async (body, httpClient = getHttpClient()) => {
|
||||
const response = await httpClient.post(`${apiPrefix}/index/add`, body);
|
||||
// Only track successful actions.
|
||||
trackUserAction(UA_POLICY_ATTACH_INDEX, httpClient);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const addLifecyclePolicyToTemplate = async (body, httpClient = getHttpClient()) => {
|
||||
const response = await httpClient.post(`${apiPrefix}/template`, body);
|
||||
// Only track successful actions.
|
||||
trackUserAction(UA_POLICY_ATTACH_INDEX_TEMPLATE, httpClient);
|
||||
return response.data;
|
||||
};
|
||||
|
|
|
@ -6,3 +6,4 @@
|
|||
|
||||
export { filterItems } from './filter_items';
|
||||
export { sortTable } from './sort_table';
|
||||
export { trackUserAction, getUserActionsForPhases } from './user_action';
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { get } from 'lodash';
|
||||
|
||||
import { createUserActionUri } from '../../../../common/user_action';
|
||||
|
||||
import {
|
||||
UA_APP_NAME,
|
||||
UA_CONFIG_COLD_PHASE,
|
||||
UA_CONFIG_WARM_PHASE,
|
||||
UA_CONFIG_SET_PRIORITY,
|
||||
UA_CONFIG_FREEZE_INDEX,
|
||||
} from '../../common/constants';
|
||||
|
||||
import {
|
||||
PHASE_HOT,
|
||||
PHASE_WARM,
|
||||
PHASE_COLD,
|
||||
PHASE_INDEX_PRIORITY,
|
||||
} from '../constants';
|
||||
|
||||
import {
|
||||
defaultColdPhase,
|
||||
defaultWarmPhase,
|
||||
defaultHotPhase,
|
||||
} from '../store/defaults';
|
||||
|
||||
import { getHttpClient } from './api';
|
||||
|
||||
export function trackUserAction(actionType, httpClient = getHttpClient()) {
|
||||
const userActionUri = createUserActionUri(UA_APP_NAME, actionType);
|
||||
httpClient.post(userActionUri);
|
||||
}
|
||||
|
||||
export function getUserActionsForPhases(phases) {
|
||||
const possibleUserActions = [{
|
||||
action: UA_CONFIG_COLD_PHASE,
|
||||
isExecuted: () => Boolean(phases[PHASE_COLD]),
|
||||
}, {
|
||||
action: UA_CONFIG_WARM_PHASE,
|
||||
isExecuted: () => Boolean(phases[PHASE_WARM]),
|
||||
}, {
|
||||
action: UA_CONFIG_SET_PRIORITY,
|
||||
isExecuted: () => {
|
||||
const phaseToDefaultIndexPriorityMap = {
|
||||
[PHASE_HOT]: defaultHotPhase[PHASE_INDEX_PRIORITY],
|
||||
[PHASE_WARM]: defaultWarmPhase[PHASE_INDEX_PRIORITY],
|
||||
[PHASE_COLD]: defaultColdPhase[PHASE_INDEX_PRIORITY],
|
||||
};
|
||||
|
||||
// We only care about whether the user has interacted with the priority of *any* phase at all.
|
||||
return [ PHASE_HOT, PHASE_WARM, PHASE_COLD ].some(phase => {
|
||||
// If the priority is different than the default, we'll consider it a user interaction,
|
||||
// even if the user has set it to undefined.
|
||||
return phases[phase] && get(phases[phase], 'actions.set_priority.priority') !== phaseToDefaultIndexPriorityMap[phase];
|
||||
});
|
||||
},
|
||||
}, {
|
||||
action: UA_CONFIG_FREEZE_INDEX,
|
||||
isExecuted: () => phases[PHASE_COLD] && get(phases[PHASE_COLD], 'actions.freeze'),
|
||||
}];
|
||||
|
||||
const executedUserActions = possibleUserActions.reduce((executed, { action, isExecuted }) => {
|
||||
if (isExecuted()) {
|
||||
executed.push(action);
|
||||
}
|
||||
return executed;
|
||||
}, []);
|
||||
|
||||
return executedUserActions;
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
UA_CONFIG_COLD_PHASE,
|
||||
UA_CONFIG_WARM_PHASE,
|
||||
UA_CONFIG_SET_PRIORITY,
|
||||
UA_CONFIG_FREEZE_INDEX,
|
||||
} from '../../common/constants';
|
||||
|
||||
import {
|
||||
defaultColdPhase,
|
||||
defaultWarmPhase,
|
||||
} from '../store/defaults';
|
||||
|
||||
import {
|
||||
PHASE_INDEX_PRIORITY,
|
||||
} from '../constants';
|
||||
|
||||
import { getUserActionsForPhases } from './user_action';
|
||||
|
||||
describe('getUserActionsForPhases', () => {
|
||||
test('gets cold phase', () => {
|
||||
expect(getUserActionsForPhases({
|
||||
cold: {
|
||||
actions: {
|
||||
set_priority: {
|
||||
priority: defaultColdPhase[PHASE_INDEX_PRIORITY],
|
||||
},
|
||||
},
|
||||
},
|
||||
})).toEqual([UA_CONFIG_COLD_PHASE]);
|
||||
});
|
||||
|
||||
test('gets warm phase', () => {
|
||||
expect(getUserActionsForPhases({
|
||||
warm: {
|
||||
actions: {
|
||||
set_priority: {
|
||||
priority: defaultWarmPhase[PHASE_INDEX_PRIORITY],
|
||||
},
|
||||
},
|
||||
},
|
||||
})).toEqual([UA_CONFIG_WARM_PHASE]);
|
||||
});
|
||||
|
||||
test(`gets index priority if it's different than the default value`, () => {
|
||||
expect(getUserActionsForPhases({
|
||||
warm: {
|
||||
actions: {
|
||||
set_priority: {
|
||||
priority: defaultWarmPhase[PHASE_INDEX_PRIORITY] + 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
})).toEqual([UA_CONFIG_WARM_PHASE, UA_CONFIG_SET_PRIORITY]);
|
||||
});
|
||||
|
||||
test('gets freeze index', () => {
|
||||
expect(getUserActionsForPhases({
|
||||
cold: {
|
||||
actions: {
|
||||
freeze: {},
|
||||
set_priority: {
|
||||
priority: defaultColdPhase[PHASE_INDEX_PRIORITY],
|
||||
},
|
||||
},
|
||||
},
|
||||
})).toEqual([UA_CONFIG_COLD_PHASE, UA_CONFIG_FREEZE_INDEX]);
|
||||
});
|
||||
});
|
|
@ -5,9 +5,15 @@
|
|||
*/
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
|
||||
import {
|
||||
UA_POLICY_CREATE,
|
||||
UA_POLICY_UPDATE,
|
||||
} from '../../../common/constants';
|
||||
|
||||
import { showApiError } from '../../services/api_errors';
|
||||
import { saveLifecycle as saveLifecycleApi } from '../../services/api';
|
||||
|
||||
import { trackUserAction, getUserActionsForPhases } from '../../services';
|
||||
|
||||
export const saveLifecyclePolicy = (lifecycle, isNew) => async () => {
|
||||
try {
|
||||
|
@ -23,6 +29,11 @@ export const saveLifecyclePolicy = (lifecycle, isNew) => async () => {
|
|||
showApiError(err, title);
|
||||
return false;
|
||||
}
|
||||
|
||||
const userActions = getUserActionsForPhases(lifecycle.phases);
|
||||
userActions.push(isNew ? UA_POLICY_CREATE : UA_POLICY_UPDATE);
|
||||
trackUserAction(userActions.join(','));
|
||||
|
||||
const message = i18n.translate('xpack.indexLifecycleMgmt.editPolicy.successfulSaveMessage',
|
||||
{
|
||||
defaultMessage: '{verb} lifecycle policy "{lifecycleName}"',
|
||||
|
|
|
@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import { createAction } from 'redux-actions';
|
||||
import { showApiError } from '../../services/api_errors';
|
||||
import { loadNodes, loadNodeDetails } from '../../services/api';
|
||||
import { SET_SELECTED_NODE_ATTRS } from '../constants';
|
||||
import { SET_SELECTED_NODE_ATTRS } from '../../constants';
|
||||
|
||||
export const setSelectedNodeAttrs = createAction(SET_SELECTED_NODE_ATTRS);
|
||||
export const setSelectedPrimaryShardCount = createAction(
|
||||
|
|
|
@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import { showApiError } from '../../services/api_errors';
|
||||
import { createAction } from 'redux-actions';
|
||||
import { loadPolicies } from '../../services/api';
|
||||
import { SET_PHASE_DATA } from '../constants';
|
||||
import { SET_PHASE_DATA } from '../../constants';
|
||||
export const fetchedPolicies = createAction('FETCHED_POLICIES');
|
||||
export const setSelectedPolicy = createAction('SET_SELECTED_POLICY');
|
||||
export const unsetSelectedPolicy = createAction('UNSET_SELECTED_POLICY');
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
PHASE_ROLLOVER_ALIAS,
|
||||
PHASE_FREEZE_ENABLED,
|
||||
PHASE_INDEX_PRIORITY,
|
||||
} from '../constants';
|
||||
} from '../../constants';
|
||||
|
||||
export const defaultColdPhase = {
|
||||
[PHASE_ENABLED]: false,
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
PHASE_ROLLOVER_MINIMUM_AGE,
|
||||
PHASE_ROLLOVER_MINIMUM_AGE_UNITS,
|
||||
PHASE_ROLLOVER_ALIAS,
|
||||
} from '../constants';
|
||||
} from '../../constants';
|
||||
|
||||
export const defaultDeletePhase = {
|
||||
[PHASE_ENABLED]: false,
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
PHASE_ROLLOVER_MAX_DOCUMENTS,
|
||||
PHASE_ROLLOVER_MAX_SIZE_STORED_UNITS,
|
||||
PHASE_INDEX_PRIORITY
|
||||
} from '../constants';
|
||||
} from '../../constants';
|
||||
|
||||
export const defaultHotPhase = {
|
||||
[PHASE_ENABLED]: true,
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
PHASE_SHRINK_ENABLED,
|
||||
WARM_PHASE_ON_ROLLOVER,
|
||||
PHASE_INDEX_PRIORITY
|
||||
} from '../constants';
|
||||
} from '../../constants';
|
||||
|
||||
export const defaultWarmPhase = {
|
||||
[PHASE_ENABLED]: false,
|
||||
|
|
|
@ -27,7 +27,7 @@ import {
|
|||
PHASE_COLD,
|
||||
PHASE_DELETE,
|
||||
PHASE_ATTRIBUTES_THAT_ARE_NUMBERS,
|
||||
} from '../constants';
|
||||
} from '../../constants';
|
||||
|
||||
import {
|
||||
defaultColdPhase,
|
||||
|
|
|
@ -25,7 +25,7 @@ import {
|
|||
WARM_PHASE_ON_ROLLOVER,
|
||||
PHASE_INDEX_PRIORITY,
|
||||
PHASE_ROLLOVER_MAX_DOCUMENTS
|
||||
} from '../constants';
|
||||
} from '../../constants';
|
||||
import {
|
||||
getPhase,
|
||||
getPhases,
|
||||
|
|
|
@ -32,7 +32,7 @@ import {
|
|||
PHASE_FREEZE_ENABLED,
|
||||
PHASE_INDEX_PRIORITY,
|
||||
PHASE_ROLLOVER_MAX_DOCUMENTS
|
||||
} from '../constants';
|
||||
} from '../../constants';
|
||||
import {
|
||||
defaultEmptyDeletePhase,
|
||||
defaultEmptyColdPhase,
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { fetchUserActions } from '../../../../server/lib/user_action';
|
||||
import { UA_APP_NAME, USER_ACTIONS } from '../../common/constants';
|
||||
|
||||
const INDEX_LIFECYCLE_MANAGEMENT_USAGE_TYPE = 'index_lifecycle_management';
|
||||
|
||||
export function registerIndexLifecycleManagementUsageCollector(server) {
|
||||
const collector = server.usage.collectorSet.makeUsageCollector({
|
||||
type: INDEX_LIFECYCLE_MANAGEMENT_USAGE_TYPE,
|
||||
fetch: async () => {
|
||||
const userActions = await fetchUserActions(server, UA_APP_NAME, USER_ACTIONS);
|
||||
|
||||
return {
|
||||
user_actions: userActions,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
server.usage.collectorSet.register(collector);
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { registerIndexLifecycleManagementUsageCollector } from './collector';
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { App } from '../../public/app';
|
||||
import { AppWithoutRouter } from '../../public/app';
|
||||
import { Provider } from 'react-redux';
|
||||
import { loadIndicesSuccess } from '../../public/store/actions';
|
||||
import { indexManagementStore } from '../../public/store';
|
||||
|
@ -123,7 +123,7 @@ describe('index table', () => {
|
|||
component = (
|
||||
<Provider store={store}>
|
||||
<MemoryRouter initialEntries={[`${BASE_PATH}indices`]}>
|
||||
<App />
|
||||
<AppWithoutRouter />
|
||||
</MemoryRouter>
|
||||
</Provider>
|
||||
);
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const UA_APP_NAME = 'index-management';
|
||||
export const UA_APP_NAME = 'index_management';
|
||||
|
||||
export const UA_APP_LOAD = 'app_load';
|
||||
export const UA_UPDATE_SETTINGS = 'update_settings';
|
||||
|
|
|
@ -4,26 +4,27 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { Switch, Route, Redirect } from 'react-router-dom';
|
||||
import React, { useEffect } from 'react';
|
||||
import { HashRouter, Switch, Route, Redirect } from 'react-router-dom';
|
||||
import { BASE_PATH, UA_APP_LOAD } from '../common/constants';
|
||||
import { IndexList } from './sections/index_list';
|
||||
import { trackUserAction } from './services';
|
||||
|
||||
export class App extends Component {
|
||||
componentWillMount() {
|
||||
trackUserAction(UA_APP_LOAD);
|
||||
}
|
||||
export const App = () => {
|
||||
useEffect(() => trackUserAction(UA_APP_LOAD), []);
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Switch>
|
||||
<Redirect exact from={`${BASE_PATH}`} to={`${BASE_PATH}indices`}/>
|
||||
<Route exact path={`${BASE_PATH}indices`} component={IndexList} />
|
||||
<Route path={`${BASE_PATH}indices/filter/:filter?`} component={IndexList}/>
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<HashRouter>
|
||||
<AppWithoutRouter />
|
||||
</HashRouter>
|
||||
);
|
||||
};
|
||||
|
||||
// Exoprt this so we can test it with a different router.
|
||||
export const AppWithoutRouter = () => (
|
||||
<Switch>
|
||||
<Redirect exact from={`${BASE_PATH}`} to={`${BASE_PATH}indices`}/>
|
||||
<Route exact path={`${BASE_PATH}indices`} component={IndexList} />
|
||||
<Route path={`${BASE_PATH}indices/filter/:filter?`} component={IndexList}/>
|
||||
</Switch>
|
||||
);
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
import React from 'react';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { HashRouter } from 'react-router-dom';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { setHttpClient } from './services/api';
|
||||
import { setUrlService } from './services/navigation';
|
||||
|
@ -28,9 +27,7 @@ const renderReact = async (elem) => {
|
|||
render(
|
||||
<I18nContext>
|
||||
<Provider store={indexManagementStore()}>
|
||||
<HashRouter>
|
||||
<App />
|
||||
</HashRouter>
|
||||
<App />
|
||||
</Provider>
|
||||
</I18nContext>,
|
||||
elem
|
||||
|
|
|
@ -6,14 +6,14 @@
|
|||
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Switch, Route, Redirect } from 'react-router-dom';
|
||||
import { HashRouter, Switch, Route, Redirect } from 'react-router-dom';
|
||||
|
||||
import { UA_APP_LOAD } from '../../common';
|
||||
import { CRUD_APP_BASE_PATH } from './constants';
|
||||
import { registerRouter, setUserHasLeftApp, trackUserAction } from './services';
|
||||
import { JobList, JobCreate } from './sections';
|
||||
|
||||
export class App extends Component {
|
||||
class ShareRouter extends Component {
|
||||
static contextTypes = {
|
||||
router: PropTypes.shape({
|
||||
history: PropTypes.shape({
|
||||
|
@ -34,7 +34,13 @@ export class App extends Component {
|
|||
registerRouter(router);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
render() {
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
export class App extends Component { // eslint-disable-line react/no-multi-comp
|
||||
componentDidMount() {
|
||||
trackUserAction(UA_APP_LOAD);
|
||||
}
|
||||
|
||||
|
@ -45,11 +51,15 @@ export class App extends Component {
|
|||
|
||||
render() {
|
||||
return (
|
||||
<Switch>
|
||||
<Redirect exact from={`${CRUD_APP_BASE_PATH}`} to={`${CRUD_APP_BASE_PATH}/job_list`} />
|
||||
<Route exact path={`${CRUD_APP_BASE_PATH}/job_list`} component={JobList} />
|
||||
<Route exact path={`${CRUD_APP_BASE_PATH}/create`} component={JobCreate} />
|
||||
</Switch>
|
||||
<HashRouter>
|
||||
<ShareRouter>
|
||||
<Switch>
|
||||
<Redirect exact from={`${CRUD_APP_BASE_PATH}`} to={`${CRUD_APP_BASE_PATH}/job_list`} />
|
||||
<Route exact path={`${CRUD_APP_BASE_PATH}/job_list`} component={JobList} />
|
||||
<Route exact path={`${CRUD_APP_BASE_PATH}/create`} component={JobCreate} />
|
||||
</Switch>
|
||||
</ShareRouter>
|
||||
</HashRouter>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@ import React from 'react';
|
|||
import { FeatureCatalogueRegistryProvider, FeatureCatalogueCategory } from 'ui/registry/feature_catalogue';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { HashRouter } from 'react-router-dom';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { I18nContext } from 'ui/i18n';
|
||||
import { management } from 'ui/management';
|
||||
|
@ -33,9 +32,7 @@ const renderReact = async (elem) => {
|
|||
render(
|
||||
<I18nContext>
|
||||
<Provider store={rollupJobsStore}>
|
||||
<HashRouter>
|
||||
<App />
|
||||
</HashRouter>
|
||||
<App />
|
||||
</Provider>
|
||||
</I18nContext>,
|
||||
elem
|
||||
|
|
|
@ -13,8 +13,16 @@ export function trackUserAction(actionType) {
|
|||
getHttp().post(userActionUri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transparently return provided request Promise, while allowing us to track
|
||||
* a successful completion of the request.
|
||||
*/
|
||||
export function trackUserRequest(request, actionType) {
|
||||
// Only track successful actions.
|
||||
request.then(() => trackUserAction(actionType));
|
||||
return request;
|
||||
return request.then(response => {
|
||||
trackUserAction(actionType);
|
||||
// We return the response immediately without waiting for the tracking request to resolve,
|
||||
// to avoid adding additional latency.
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue