Skip to content

Commit b7b942e

Browse files
authored
add client-side feature usage API (#75486) (#76256)
* add client-side feature_usage API * use route context for notify feature usage route
1 parent ee7c071 commit b7b942e

File tree

14 files changed

+384
-3
lines changed

14 files changed

+384
-3
lines changed

x-pack/plugins/licensing/public/mocks.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66
import { BehaviorSubject } from 'rxjs';
77
import { LicensingPluginSetup, LicensingPluginStart } from './types';
88
import { licenseMock } from '../common/licensing.mock';
9+
import { featureUsageMock } from './services/feature_usage_service.mock';
910

1011
const createSetupMock = () => {
1112
const license = licenseMock.createLicense();
1213
const mock: jest.Mocked<LicensingPluginSetup> = {
1314
license$: new BehaviorSubject(license),
1415
refresh: jest.fn(),
16+
featureUsage: featureUsageMock.createSetup(),
1517
};
1618
mock.refresh.mockResolvedValue(license);
1719

@@ -23,6 +25,7 @@ const createStartMock = () => {
2325
const mock: jest.Mocked<LicensingPluginStart> = {
2426
license$: new BehaviorSubject(license),
2527
refresh: jest.fn(),
28+
featureUsage: featureUsageMock.createStart(),
2629
};
2730
mock.refresh.mockResolvedValue(license);
2831

x-pack/plugins/licensing/public/plugin.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
import { Observable, Subject, Subscription } from 'rxjs';
77

88
import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public';
9-
109
import { ILicense } from '../common/types';
1110
import { LicensingPluginSetup, LicensingPluginStart } from './types';
1211
import { createLicenseUpdate } from '../common/license_update';
1312
import { License } from '../common/license';
1413
import { mountExpiredBanner } from './expired_banner';
14+
import { FeatureUsageService } from './services';
1515

1616
export const licensingSessionStorageKey = 'xpack.licensing';
1717

@@ -39,6 +39,7 @@ export class LicensingPlugin implements Plugin<LicensingPluginSetup, LicensingPl
3939

4040
private refresh?: () => Promise<ILicense>;
4141
private license$?: Observable<ILicense>;
42+
private featureUsage = new FeatureUsageService();
4243

4344
constructor(
4445
context: PluginInitializerContext,
@@ -116,6 +117,7 @@ export class LicensingPlugin implements Plugin<LicensingPluginSetup, LicensingPl
116117
return {
117118
refresh: refreshManually,
118119
license$,
120+
featureUsage: this.featureUsage.setup({ http: core.http }),
119121
};
120122
}
121123

@@ -127,6 +129,7 @@ export class LicensingPlugin implements Plugin<LicensingPluginSetup, LicensingPl
127129
return {
128130
refresh: this.refresh,
129131
license$: this.license$,
132+
featureUsage: this.featureUsage.start({ http: core.http }),
130133
};
131134
}
132135

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import {
8+
FeatureUsageService,
9+
FeatureUsageServiceSetup,
10+
FeatureUsageServiceStart,
11+
} from './feature_usage_service';
12+
13+
const createSetupMock = (): jest.Mocked<FeatureUsageServiceSetup> => {
14+
const mock = {
15+
register: jest.fn(),
16+
};
17+
18+
return mock;
19+
};
20+
21+
const createStartMock = (): jest.Mocked<FeatureUsageServiceStart> => {
22+
const mock = {
23+
notifyUsage: jest.fn(),
24+
};
25+
26+
return mock;
27+
};
28+
29+
const createServiceMock = (): jest.Mocked<PublicMethodsOf<FeatureUsageService>> => {
30+
const mock = {
31+
setup: jest.fn(),
32+
start: jest.fn(),
33+
};
34+
35+
mock.setup.mockImplementation(() => createSetupMock());
36+
mock.start.mockImplementation(() => createStartMock());
37+
38+
return mock;
39+
};
40+
41+
export const featureUsageMock = {
42+
create: createServiceMock,
43+
createSetup: createSetupMock,
44+
createStart: createStartMock,
45+
};
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { httpServiceMock } from '../../../../../src/core/public/mocks';
8+
import { FeatureUsageService } from './feature_usage_service';
9+
10+
describe('FeatureUsageService', () => {
11+
let http: ReturnType<typeof httpServiceMock.createSetupContract>;
12+
let service: FeatureUsageService;
13+
14+
beforeEach(() => {
15+
http = httpServiceMock.createSetupContract();
16+
service = new FeatureUsageService();
17+
});
18+
19+
describe('#setup', () => {
20+
describe('#register', () => {
21+
it('calls the endpoint with the correct parameters', async () => {
22+
const setup = service.setup({ http });
23+
await setup.register('my-feature', 'platinum');
24+
expect(http.post).toHaveBeenCalledTimes(1);
25+
expect(http.post).toHaveBeenCalledWith('/internal/licensing/feature_usage/register', {
26+
body: JSON.stringify({
27+
featureName: 'my-feature',
28+
licenseType: 'platinum',
29+
}),
30+
});
31+
});
32+
});
33+
});
34+
35+
describe('#start', () => {
36+
describe('#notifyUsage', () => {
37+
it('calls the endpoint with the correct parameters', async () => {
38+
service.setup({ http });
39+
const start = service.start({ http });
40+
await start.notifyUsage('my-feature', 42);
41+
42+
expect(http.post).toHaveBeenCalledTimes(1);
43+
expect(http.post).toHaveBeenCalledWith('/internal/licensing/feature_usage/notify', {
44+
body: JSON.stringify({
45+
featureName: 'my-feature',
46+
lastUsed: 42,
47+
}),
48+
});
49+
});
50+
51+
it('correctly convert dates', async () => {
52+
service.setup({ http });
53+
const start = service.start({ http });
54+
55+
const now = new Date();
56+
57+
await start.notifyUsage('my-feature', now);
58+
59+
expect(http.post).toHaveBeenCalledTimes(1);
60+
expect(http.post).toHaveBeenCalledWith('/internal/licensing/feature_usage/notify', {
61+
body: JSON.stringify({
62+
featureName: 'my-feature',
63+
lastUsed: now.getTime(),
64+
}),
65+
});
66+
});
67+
});
68+
});
69+
});
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import isDate from 'lodash/isDate';
8+
import type { HttpSetup, HttpStart } from 'src/core/public';
9+
import { LicenseType } from '../../common/types';
10+
11+
/** @public */
12+
export interface FeatureUsageServiceSetup {
13+
/**
14+
* Register a feature to be able to notify of it's usages using the {@link FeatureUsageServiceStart | service start contract}.
15+
*/
16+
register(featureName: string, licenseType: LicenseType): Promise<void>;
17+
}
18+
19+
/** @public */
20+
export interface FeatureUsageServiceStart {
21+
/**
22+
* Notify of a registered feature usage at given time.
23+
*
24+
* @param featureName - the name of the feature to notify usage of
25+
* @param usedAt - Either a `Date` or an unix timestamp with ms. If not specified, it will be set to the current time.
26+
*/
27+
notifyUsage(featureName: string, usedAt?: Date | number): Promise<void>;
28+
}
29+
30+
interface SetupDeps {
31+
http: HttpSetup;
32+
}
33+
34+
interface StartDeps {
35+
http: HttpStart;
36+
}
37+
38+
/**
39+
* @internal
40+
*/
41+
export class FeatureUsageService {
42+
public setup({ http }: SetupDeps): FeatureUsageServiceSetup {
43+
return {
44+
register: async (featureName, licenseType) => {
45+
await http.post('/internal/licensing/feature_usage/register', {
46+
body: JSON.stringify({
47+
featureName,
48+
licenseType,
49+
}),
50+
});
51+
},
52+
};
53+
}
54+
55+
public start({ http }: StartDeps): FeatureUsageServiceStart {
56+
return {
57+
notifyUsage: async (featureName, usedAt = Date.now()) => {
58+
const lastUsed = isDate(usedAt) ? usedAt.getTime() : usedAt;
59+
await http.post('/internal/licensing/feature_usage/notify', {
60+
body: JSON.stringify({
61+
featureName,
62+
lastUsed,
63+
}),
64+
});
65+
},
66+
};
67+
}
68+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
export {
8+
FeatureUsageService,
9+
FeatureUsageServiceSetup,
10+
FeatureUsageServiceStart,
11+
} from './feature_usage_service';

x-pack/plugins/licensing/public/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import { Observable } from 'rxjs';
77

88
import { ILicense } from '../common/types';
9+
import { FeatureUsageServiceSetup, FeatureUsageServiceStart } from './services';
910

1011
/** @public */
1112
export interface LicensingPluginSetup {
@@ -19,6 +20,10 @@ export interface LicensingPluginSetup {
1920
* @deprecated in favour of the counterpart provided from start contract
2021
*/
2122
refresh(): Promise<ILicense>;
23+
/**
24+
* APIs to register licensed feature usage.
25+
*/
26+
featureUsage: FeatureUsageServiceSetup;
2227
}
2328

2429
/** @public */
@@ -31,4 +36,8 @@ export interface LicensingPluginStart {
3136
* Triggers licensing information re-fetch.
3237
*/
3338
refresh(): Promise<ILicense>;
39+
/**
40+
* APIs to manage licensed feature usage.
41+
*/
42+
featureUsage: FeatureUsageServiceStart;
3443
}

x-pack/plugins/licensing/server/plugin.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,9 @@ export class LicensingPlugin implements Plugin<LicensingPluginSetup, LicensingPl
133133
createRouteHandlerContext(license$, core.getStartServices)
134134
);
135135

136-
registerRoutes(core.http.createRouter(), core.getStartServices);
136+
const featureUsageSetup = this.featureUsage.setup();
137+
138+
registerRoutes(core.http.createRouter(), featureUsageSetup, core.getStartServices);
137139
core.http.registerOnPreResponse(createOnPreResponseHandler(refresh, license$));
138140

139141
this.refresh = refresh;
@@ -143,7 +145,7 @@ export class LicensingPlugin implements Plugin<LicensingPluginSetup, LicensingPl
143145
refresh,
144146
license$,
145147
createLicensePoller: this.createLicensePoller.bind(this),
146-
featureUsage: this.featureUsage.setup(),
148+
featureUsage: featureUsageSetup,
147149
};
148150
}
149151

x-pack/plugins/licensing/server/routes/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,18 @@
66

77
import { IRouter, StartServicesAccessor } from 'src/core/server';
88
import { LicensingPluginStart } from '../types';
9+
import { FeatureUsageServiceSetup } from '../services';
910
import { registerInfoRoute } from './info';
1011
import { registerFeatureUsageRoute } from './feature_usage';
12+
import { registerNotifyFeatureUsageRoute, registerRegisterFeatureRoute } from './internal';
1113

1214
export function registerRoutes(
1315
router: IRouter,
16+
featureUsageSetup: FeatureUsageServiceSetup,
1417
getStartServices: StartServicesAccessor<{}, LicensingPluginStart>
1518
) {
1619
registerInfoRoute(router);
1720
registerFeatureUsageRoute(router, getStartServices);
21+
registerRegisterFeatureRoute(router, featureUsageSetup);
22+
registerNotifyFeatureUsageRoute(router);
1823
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
export { registerNotifyFeatureUsageRoute } from './notify_feature_usage';
8+
export { registerRegisterFeatureRoute } from './register_feature';

0 commit comments

Comments
 (0)