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:
CJ Cenizal 2019-03-26 15:43:03 -07:00 committed by GitHub
parent 7676f03bca
commit d8c7e18bf1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 400 additions and 89 deletions

View file

@ -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 });

View file

@ -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'));
});
});
});

View file

@ -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';

View file

@ -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,
];

View file

@ -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 &&

View file

@ -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>
);
};

View file

@ -17,7 +17,7 @@ import {
PHASE_COLD,
PHASE_HOT,
PHASE_ROLLOVER_ENABLED
} from '../../../../store/constants';
} from '../../../../constants';
export const ColdPhase = connect(
(state) => ({

View file

@ -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';

View file

@ -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 => ({

View file

@ -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 {

View file

@ -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 => ({

View file

@ -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';

View file

@ -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 => {

View file

@ -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 = (

View file

@ -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 => {

View file

@ -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 => ({

View file

@ -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';

View file

@ -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';

View file

@ -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>

View file

@ -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;
};

View file

@ -6,3 +6,4 @@
export { filterItems } from './filter_items';
export { sortTable } from './sort_table';
export { trackUserAction, getUserActionsForPhases } from './user_action';

View file

@ -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;
}

View file

@ -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]);
});
});

View file

@ -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}"',

View file

@ -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(

View file

@ -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');

View file

@ -12,7 +12,7 @@ import {
PHASE_ROLLOVER_ALIAS,
PHASE_FREEZE_ENABLED,
PHASE_INDEX_PRIORITY,
} from '../constants';
} from '../../constants';
export const defaultColdPhase = {
[PHASE_ENABLED]: false,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -27,7 +27,7 @@ import {
PHASE_COLD,
PHASE_DELETE,
PHASE_ATTRIBUTES_THAT_ARE_NUMBERS,
} from '../constants';
} from '../../constants';
import {
defaultColdPhase,

View file

@ -25,7 +25,7 @@ import {
WARM_PHASE_ON_ROLLOVER,
PHASE_INDEX_PRIORITY,
PHASE_ROLLOVER_MAX_DOCUMENTS
} from '../constants';
} from '../../constants';
import {
getPhase,
getPhases,

View file

@ -32,7 +32,7 @@ import {
PHASE_FREEZE_ENABLED,
PHASE_INDEX_PRIORITY,
PHASE_ROLLOVER_MAX_DOCUMENTS
} from '../constants';
} from '../../constants';
import {
defaultEmptyDeletePhase,
defaultEmptyColdPhase,

View file

@ -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);
}

View file

@ -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';

View file

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

View file

@ -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';

View file

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

View file

@ -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

View file

@ -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>
);
}
}

View file

@ -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

View file

@ -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;
});
}