Skip to content

Commit

Permalink
Dashboard favorites telemetry (#190706)
Browse files Browse the repository at this point in the history
## Summary

Add telemetry to favorites feature
#189285


- Adds UI usage counter telemetry, increase the counter when favorite /
unfavorite is clicked
- Add snapshot telemetry: 
  - total "favorite" object in the deployment 
- total users+spaces count combination who have used the favorites
feature
- avg per user per space (only counts those users who favorited at least
once)
  - max favorites objects per user per space

Unfortunately, for snapshot telemetry, I had to add fields to kibana
mapping. We didn't need them for a feature, but I didn't realize that
will have to add them to a mapping. Not sure if there is a better way
  • Loading branch information
Dosant authored Aug 29, 2024
1 parent 7d54e4e commit ec0230b
Show file tree
Hide file tree
Showing 24 changed files with 317 additions and 26 deletions.
4 changes: 3 additions & 1 deletion packages/content-management/favorites/README.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@ import {
FavoriteButton,
} from '@kbn/content-management-favorites-public';

const appName = 'my-app';
const favoriteObjectType = 'dashboard';
const favoritesClient = new FavoritesClient('dashboard', {
const favoritesClient = new FavoritesClient(appName, favoriteObjectType, {
http: core.http,
usageCollection: plugins.usageCollection,
});

// wrap your content with the favorites context provider
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import classNames from 'classnames';
import { EuiButtonIcon, euiCanAnimate, EuiThemeComputed } from '@elastic/eui';
import { css } from '@emotion/react';
import { useFavorites, useRemoveFavorite, useAddFavorite } from '../favorites_query';
import { useFavoritesClient } from '../favorites_context';

export interface FavoriteButtonProps {
id: string;
Expand All @@ -24,6 +25,8 @@ export const FavoriteButton = ({ id, className }: FavoriteButtonProps) => {
const removeFavorite = useRemoveFavorite();
const addFavorite = useAddFavorite();

const favoritesClient = useFavoritesClient();

if (!data) return null;

const isFavorite = data.favoriteIds.includes(id);
Expand All @@ -40,6 +43,7 @@ export const FavoriteButton = ({ id, className }: FavoriteButtonProps) => {
aria-label={title}
iconType={'starFilled'}
onClick={() => {
favoritesClient?.reportRemoveFavoriteClick();
removeFavorite.mutate({ id });
}}
className={classNames(className, 'cm-favorite-button', {
Expand All @@ -59,6 +63,7 @@ export const FavoriteButton = ({ id, className }: FavoriteButtonProps) => {
aria-label={title}
iconType={'starEmpty'}
onClick={() => {
favoritesClient?.reportAddFavoriteClick();
addFavorite.mutate({ id });
}}
className={classNames(className, 'cm-favorite-button', {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

import type { HttpStart } from '@kbn/core-http-browser';
import type { UsageCollectionStart } from '@kbn/usage-collection-plugin/public';
import type { GetFavoritesResponse } from '@kbn/content-management-favorites-server';

export interface FavoritesClientPublic {
Expand All @@ -15,10 +16,16 @@ export interface FavoritesClientPublic {
removeFavorite({ id }: { id: string }): Promise<GetFavoritesResponse>;

getFavoriteType(): string;
reportAddFavoriteClick(): void;
reportRemoveFavoriteClick(): void;
}

export class FavoritesClient implements FavoritesClientPublic {
constructor(private favoriteObjectType: string, private deps: { http: HttpStart }) {}
constructor(
private readonly appName: string,
private readonly favoriteObjectType: string,
private readonly deps: { http: HttpStart; usageCollection?: UsageCollectionStart }
) {}

public async getFavorites(): Promise<GetFavoritesResponse> {
return this.deps.http.get(`/internal/content_management/favorites/${this.favoriteObjectType}`);
Expand All @@ -39,4 +46,11 @@ export class FavoritesClient implements FavoritesClientPublic {
public getFavoriteType() {
return this.favoriteObjectType;
}

public reportAddFavoriteClick() {
this.deps.usageCollection?.reportUiCounter(this.appName, 'click', 'add_favorite');
}
public reportRemoveFavoriteClick() {
this.deps.usageCollection?.reportUiCounter(this.appName, 'click', 'remove_favorite');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@
"@kbn/core-http-browser",
"@kbn/content-management-favorites-server",
"@kbn/i18n-react",
"@kbn/usage-collection-plugin",
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,19 @@ const schemaV1 = schema.object({
favoriteIds: schema.arrayOf(schema.string()),
});

export const favoritesSavedObjectName = 'favorites';

export const favoritesSavedObjectType: SavedObjectsType = {
name: 'favorites',
name: favoritesSavedObjectName,
hidden: true,
namespaceType: 'single',
mappings: {
dynamic: false,
properties: {},
properties: {
userId: { type: 'keyword' },
type: { type: 'keyword' },
favoriteIds: { type: 'keyword' },
},
},
modelVersions: {
1: {
Expand All @@ -41,5 +47,22 @@ export const favoritesSavedObjectType: SavedObjectsType = {
create: schemaV1,
},
},
2: {
// the model stays the same, but we added the mappings for the snapshot telemetry needs
changes: [
{
type: 'mappings_addition',
addedMappings: {
userId: { type: 'keyword' },
type: { type: 'keyword' },
favoriteIds: { type: 'keyword' },
},
},
],
schemas: {
forwardCompatibility: schemaV1.extends({}, { unknowns: 'ignore' }),
create: schemaV1,
},
},
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import type { CoreSetup } from '@kbn/core-lifecycle-server';
import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';
import type { estypes } from '@elastic/elasticsearch';
import { favoritesSavedObjectName } from './favorites_saved_object';

interface FavoritesUsage {
[favorite_object_type: string]: {
total: number;
total_users_spaces: number;
avg_per_user_per_space: number;
max_per_user_per_space: number;
};
}

export function registerFavoritesUsageCollection({
core,
usageCollection,
}: {
core: CoreSetup;
usageCollection: UsageCollectionSetup;
}) {
usageCollection.registerCollector(
usageCollection.makeUsageCollector<FavoritesUsage>({
type: 'favorites',
isReady: () => true,
schema: {
DYNAMIC_KEY /* e.g. 'dashboard' */: {
total: {
type: 'long',
_meta: { description: 'Total favorite object count in this deployment' },
},
total_users_spaces: {
type: 'long',
_meta: {
description:
'Total users per space that have favorited an object of this type in this deployment',
},
},
avg_per_user_per_space: {
type: 'double',
_meta: {
description:
'Average favorite objects count of this type per user per space for this deployment, only counts users who have favorited at least one object of this type',
},
},
max_per_user_per_space: {
type: 'long',
_meta: {
description:
'Max favorite objects count of this type per user per space for this deployment',
},
},
},
},
fetch: async (context) => {
const favoritesIndex = await core
.getStartServices()
.then(([{ savedObjects }]) => savedObjects.getIndexForType(favoritesSavedObjectName));

const response = await context.esClient.search<
unknown,
{ types: estypes.AggregationsStringTermsAggregate }
>({
index: favoritesIndex,
size: 0,
_source: false,
filter_path: ['aggregations'],
query: {
bool: {
filter: [
{
term: {
type: 'favorites',
},
},
],
},
},
runtime_mappings: {
number_of_favorites: {
type: 'long',
script: {
source: "emit(doc['favorites.favoriteIds'].length)",
},
},
},
aggs: {
types: {
terms: {
field: 'favorites.type',
},
aggs: {
stats: {
stats: {
field: 'number_of_favorites',
},
},
},
},
},
});

const favoritesUsage: FavoritesUsage = {};

const typesBuckets = (response.aggregations?.types?.buckets ??
[]) as estypes.AggregationsStringTermsBucket[];

typesBuckets.forEach((bucket) => {
favoritesUsage[bucket.key] = {
total: bucket.stats.sum,
total_users_spaces: bucket.stats.count,
avg_per_user_per_space: bucket.stats.avg,
max_per_user_per_space: bucket.stats.max,
};
});

return favoritesUsage;
},
})
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
*/

import type { CoreSetup, Logger } from '@kbn/core/server';
import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';
import { registerFavoritesRoutes } from './favorites_routes';
import { favoritesSavedObjectType } from './favorites_saved_object';
import { registerFavoritesUsageCollection } from './favorites_usage_collection';

export type { GetFavoritesResponse } from './favorites_routes';

Expand All @@ -18,8 +20,21 @@ export type { GetFavoritesResponse } from './favorites_routes';
*
* @param logger
* @param core
* @param usageCollection
*/
export function registerFavorites({ logger, core }: { core: CoreSetup; logger: Logger }) {
export function registerFavorites({
logger,
core,
usageCollection,
}: {
core: CoreSetup;
logger: Logger;
usageCollection?: UsageCollectionSetup;
}) {
core.savedObjects.registerType(favoritesSavedObjectType);
registerFavoritesRoutes({ core, logger });

if (usageCollection) {
registerFavoritesUsageCollection({ core, usageCollection });
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,7 @@
"@kbn/core",
"@kbn/config-schema",
"@kbn/core-saved-objects-api-server",
"@kbn/core-lifecycle-server",
"@kbn/usage-collection-plugin",
]
}
6 changes: 5 additions & 1 deletion packages/kbn-check-mappings-update-cli/current_fields.json
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,11 @@
"updated_by",
"version"
],
"favorites": [],
"favorites": [
"favoriteIds",
"type",
"userId"
],
"file": [
"FileKind",
"Meta",
Expand Down
12 changes: 11 additions & 1 deletion packages/kbn-check-mappings-update-cli/current_mappings.json
Original file line number Diff line number Diff line change
Expand Up @@ -1484,7 +1484,17 @@
},
"favorites": {
"dynamic": false,
"properties": {}
"properties": {
"favoriteIds": {
"type": "keyword"
},
"type": {
"type": "keyword"
},
"userId": {
"type": "keyword"
}
}
},
"file": {
"dynamic": false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"event_loop_delays_daily": "01b967e8e043801357503de09199dfa3853bab88",
"exception-list": "4aebc4e61fb5d608cae48eaeb0977e8db21c61a4",
"exception-list-agnostic": "6d3262d58eee28ac381ec9654f93126a58be6f5d",
"favorites": "ef282e9fb5a91df3cc88409a9f86d993fb51a6e9",
"favorites": "a68c7c8ae22eaddcca324d8b3bfc80a94e3eec3a",
"file": "6b65ae5899b60ebe08656fd163ea532e557d3c98",
"file-upload-usage-collection-telemetry": "06e0a8c04f991e744e09d03ab2bd7f86b2088200",
"fileShare": "5be52de1747d249a221b5241af2838264e19aaa1",
Expand Down
5 changes: 4 additions & 1 deletion src/plugins/content_management/kibana.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
"plugin": {
"id": "contentManagement",
"server": true,
"browser": true
"browser": true,
"optionalPlugins": [
"usageCollection"
]
}
}
Loading

0 comments on commit ec0230b

Please sign in to comment.