mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Add user_action app for gathering telemetry on user actions (#31543)
* Add user_action app with API endpoint for tracking user actions. * Track 'Create rollup job' action and publish it in the collector. * Extract fetchUserActions into a generic helper inside of xpack/server/lib. * Add trackUserRequest helper.
This commit is contained in:
parent
a949da2437
commit
554e7be400
17 changed files with 293 additions and 1 deletions
35
src/legacy/core_plugins/user_action/index.js
Normal file
35
src/legacy/core_plugins/user_action/index.js
Normal file
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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 { registerUserActionRoute } from './server/routes/api/user_action';
|
||||
|
||||
export default function (kibana) {
|
||||
return new kibana.Plugin({
|
||||
id: 'user_action',
|
||||
require: ['kibana', 'elasticsearch'],
|
||||
|
||||
uiExports: {
|
||||
mappings: require('./mappings.json'),
|
||||
},
|
||||
|
||||
init: function (server) {
|
||||
registerUserActionRoute(server);
|
||||
}
|
||||
});
|
||||
}
|
9
src/legacy/core_plugins/user_action/mappings.json
Normal file
9
src/legacy/core_plugins/user_action/mappings.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"user-action": {
|
||||
"properties": {
|
||||
"count": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
4
src/legacy/core_plugins/user_action/package.json
Normal file
4
src/legacy/core_plugins/user_action/package.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"name": "user_action",
|
||||
"version": "kibana"
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* 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 Boom from 'boom';
|
||||
import { Server } from 'hapi';
|
||||
|
||||
export const registerUserActionRoute = (server: Server) => {
|
||||
/*
|
||||
* Increment a count on an object representing a specific user action.
|
||||
*/
|
||||
server.route({
|
||||
path: '/api/user_action/{appName}/{actionType}',
|
||||
method: 'POST',
|
||||
handler: async (request: any) => {
|
||||
const { appName, actionType } = 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');
|
||||
|
||||
return {};
|
||||
} catch (error) {
|
||||
return new Boom('Something went wrong', { statusCode: error.status });
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
|
@ -32,5 +32,6 @@ export default function ({ loadTestFile }) {
|
|||
loadTestFile(require.resolve('./suggestions'));
|
||||
loadTestFile(require.resolve('./status'));
|
||||
loadTestFile(require.resolve('./stats'));
|
||||
loadTestFile(require.resolve('./user_action'));
|
||||
});
|
||||
}
|
||||
|
|
24
test/api_integration/apis/user_action/index.js
Normal file
24
test/api_integration/apis/user_action/index.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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 default function ({ loadTestFile }) {
|
||||
describe('User Action', () => {
|
||||
loadTestFile(require.resolve('./user_action'));
|
||||
});
|
||||
}
|
45
test/api_integration/apis/user_action/user_action.js
Normal file
45
test/api_integration/apis/user_action/user_action.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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 expect from 'expect.js';
|
||||
import { get } from 'lodash';
|
||||
|
||||
export default function ({ getService }) {
|
||||
const supertest = getService('supertest');
|
||||
const es = getService('es');
|
||||
|
||||
describe('user_action API', () => {
|
||||
it('increments the count field in the document defined by the {app}/{action_type} path', async () => {
|
||||
await supertest
|
||||
.post('/api/user_action/myApp/myAction')
|
||||
.set('kbn-xsrf', 'kibana')
|
||||
.expect(200);
|
||||
|
||||
return es.search({
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
7
x-pack/common/user_action/index.ts
Normal file
7
x-pack/common/user_action/index.ts
Normal 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 { createUserActionUri } from './user_action';
|
11
x-pack/common/user_action/user_action.ts
Normal file
11
x-pack/common/user_action/user_action.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* 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 chrome from 'ui/chrome';
|
||||
|
||||
export function createUserActionUri(appName: string, actionType: string): string {
|
||||
return chrome.addBasePath(`/api/user_action/${appName}/${actionType}`);
|
||||
}
|
|
@ -7,3 +7,10 @@
|
|||
export const PLUGIN = {
|
||||
ID: 'rollup'
|
||||
};
|
||||
|
||||
export const UA_APP_NAME = 'rollup-job-wizard';
|
||||
export const UA_ROLLUP_JOB_CREATE = 'create';
|
||||
|
||||
export const USER_ACTIONS = [
|
||||
UA_ROLLUP_JOB_CREATE,
|
||||
];
|
||||
|
|
|
@ -5,7 +5,9 @@
|
|||
*/
|
||||
|
||||
import chrome from 'ui/chrome';
|
||||
import { UA_ROLLUP_JOB_CREATE } from '../../../common';
|
||||
import { getHttp } from './http_provider';
|
||||
import { trackUserRequest } from './track_user_action';
|
||||
|
||||
const apiPrefix = chrome.addBasePath('/api/rollup');
|
||||
|
||||
|
@ -31,7 +33,8 @@ export async function deleteJobs(jobIds) {
|
|||
|
||||
export async function createJob(job) {
|
||||
const body = { job };
|
||||
return await getHttp().put(`${apiPrefix}/create`, body);
|
||||
const request = getHttp().put(`${apiPrefix}/create`, body);
|
||||
return await trackUserRequest(request, UA_ROLLUP_JOB_CREATE);
|
||||
}
|
||||
|
||||
export async function validateIndexPattern(indexPattern) {
|
||||
|
|
|
@ -92,3 +92,7 @@ export {
|
|||
export {
|
||||
sortTable,
|
||||
} from './sort_table';
|
||||
|
||||
export {
|
||||
trackUserAction,
|
||||
} from './track_user_action';
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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 { createUserActionUri } from '../../../../../common/user_action';
|
||||
import { UA_APP_NAME } from '../../../common';
|
||||
import { getHttp } from './http_provider';
|
||||
|
||||
export function trackUserAction(actionType) {
|
||||
const userActionUri = createUserActionUri(UA_APP_NAME, actionType);
|
||||
getHttp().post(userActionUri);
|
||||
}
|
||||
|
||||
export function trackUserRequest(request, actionType) {
|
||||
// Only track successful actions.
|
||||
request.then(() => trackUserAction(actionType));
|
||||
return request;
|
||||
}
|
|
@ -5,6 +5,8 @@
|
|||
*/
|
||||
|
||||
import { get } from 'lodash';
|
||||
import { fetchUserActions } from '../../../../server/lib/user_action';
|
||||
import { UA_APP_NAME, USER_ACTIONS } from '../../common';
|
||||
|
||||
const ROLLUP_USAGE_TYPE = 'rollups';
|
||||
|
||||
|
@ -180,6 +182,8 @@ export function registerRollupUsageCollector(server) {
|
|||
rollupVisualizationsFromSavedSearches,
|
||||
} = await fetchRollupVisualizations(kibanaIndex, callCluster, rollupIndexPatternToFlagMap, rollupSavedSearchesToFlagMap);
|
||||
|
||||
const userActions = await fetchUserActions(server, UA_APP_NAME, USER_ACTIONS);
|
||||
|
||||
return {
|
||||
index_patterns: {
|
||||
total: rollupIndexPatterns.length,
|
||||
|
@ -193,6 +197,7 @@ export function registerRollupUsageCollector(server) {
|
|||
total: rollupVisualizationsFromSavedSearches,
|
||||
},
|
||||
},
|
||||
user_actions: userActions,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
3
x-pack/plugins/rollup/tsconfig.json
Normal file
3
x-pack/plugins/rollup/tsconfig.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json"
|
||||
}
|
59
x-pack/server/lib/user_action/fetch_user_actions.ts
Normal file
59
x-pack/server/lib/user_action/fetch_user_actions.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* 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 { Server } from 'hapi';
|
||||
|
||||
export interface UserActionRecord {
|
||||
actionType: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface UserActionAndCountKeyValuePair {
|
||||
key: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
// This is a helper method for retrieving user action telemetry data stored via the OSS
|
||||
// user_action API.
|
||||
export function fetchUserActions(
|
||||
server: Server,
|
||||
appName: string,
|
||||
actionTypes: string[]
|
||||
): Promise<UserActionAndCountKeyValuePair[]> {
|
||||
const { SavedObjectsClient, getSavedObjectsRepository } = server.savedObjects;
|
||||
const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin');
|
||||
const internalRepository = getSavedObjectsRepository(callWithInternalUser);
|
||||
const savedObjectsClient = new SavedObjectsClient(internalRepository);
|
||||
|
||||
async function fetchUserAction(actionType: string): Promise<UserActionRecord | undefined> {
|
||||
try {
|
||||
const savedObjectId = `${appName}:${actionType}`;
|
||||
const savedObject = await savedObjectsClient.get('user-action', savedObjectId);
|
||||
return { actionType, count: savedObject.attributes.count };
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.all(actionTypes.map(fetchUserAction)).then(
|
||||
(userActions): UserActionAndCountKeyValuePair[] => {
|
||||
const userActionAndCountKeyValuePairs = userActions.reduce(
|
||||
(pairs: UserActionAndCountKeyValuePair[], userAction: UserActionRecord | undefined) => {
|
||||
// User action is undefined if nobody has performed this action on the client yet.
|
||||
if (userAction !== undefined) {
|
||||
const { actionType, count } = userAction;
|
||||
const pair: UserActionAndCountKeyValuePair = { key: actionType, value: count };
|
||||
pairs.push(pair);
|
||||
}
|
||||
return pairs;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return userActionAndCountKeyValuePairs;
|
||||
}
|
||||
);
|
||||
}
|
7
x-pack/server/lib/user_action/index.ts
Normal file
7
x-pack/server/lib/user_action/index.ts
Normal 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 { fetchUserActions } from './fetch_user_actions';
|
Loading…
Add table
Add a link
Reference in a new issue