Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
b90194d
favorites telemetry
Dosant Aug 19, 2024
2933d91
[CI] Auto-commit changed files from 'node scripts/lint_ts_projects --…
kibanamachine Aug 19, 2024
fb9f149
wip
Dosant Aug 19, 2024
289bcfd
Merge branch 'd/2024-08-14-favorites-telemetry' of github.com:Dosant/…
Dosant Aug 19, 2024
a215da3
Merge branch 'main' of github.com:elastic/kibana into d/2024-08-14-fa…
Dosant Aug 19, 2024
2408e74
update telemetry mapping
Dosant Aug 19, 2024
e01c97e
Merge branch 'main' of github.com:elastic/kibana into d/2024-08-14-fa…
Dosant Aug 21, 2024
bfb2959
wip
Dosant Aug 21, 2024
30a527e
wip
Dosant Aug 21, 2024
c75f060
[CI] Auto-commit changed files from 'node scripts/check_mappings_upda…
kibanamachine Aug 21, 2024
159f689
fix ts
Dosant Aug 21, 2024
eda9054
Merge branch 'd/2024-08-14-favorites-telemetry' of github.com:Dosant/…
Dosant Aug 21, 2024
db55912
fix types
Dosant Aug 21, 2024
5b7e326
Merge branch 'main' of github.com:elastic/kibana into d/2024-08-14-fa…
Dosant Aug 26, 2024
d820392
add total_users_spaces
Dosant Aug 26, 2024
8ab2d24
Merge branch 'main' into d/2024-08-14-favorites-telemetry
elasticmachine Aug 28, 2024
b1a24e9
review improvements
Dosant Aug 29, 2024
3d266af
Merge branch 'main' of github.com:elastic/kibana into d/2024-08-14-fa…
Dosant Aug 29, 2024
e5165bb
[CI] Auto-commit changed files from 'node scripts/notice'
kibanamachine Aug 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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: {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We didn't need these mappings for the feature, but, unfortunately, I didn't know that we would need them for the snapshot telemetry. I am not sure if there is another better way

the object schema stays the same, but we add existing fields to the mapping

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's been my experience as well that it's sometimes necessary to add mappings just for the sake of being able to collect snapshot telemetry.

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)",
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this is the best way, but I like how clean it is to get sum, avg, max.

I've also tried value_count aggregation which works well for total, but haven't figure how it can be used for avg and max

Copy link
Copy Markdown
Member

@tsullivan tsullivan Aug 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need other alternatives to consider, there is also the stats aggregation.

Running the aggregation on a runtime field seems like a perfect idea for this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I updated to stats

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