Skip to content

Commit ce47ef5

Browse files
Updating the licensed feature usage API response format (#67712)
Co-authored-by: Elastic Machine <[email protected]>
1 parent daf26b9 commit ce47ef5

File tree

5 files changed

+98
-56
lines changed

5 files changed

+98
-56
lines changed

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

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,13 @@ export function registerFeatureUsageRoute(
1515
async (context, request, response) => {
1616
const [, , { featureUsage }] = await getStartServices();
1717
return response.ok({
18-
body: [...featureUsage.getLastUsages().entries()].reduce(
19-
(res, [featureName, lastUsage]) => {
20-
return {
21-
...res,
22-
[featureName]: new Date(lastUsage).toISOString(),
23-
};
24-
},
25-
{}
26-
),
18+
body: {
19+
features: featureUsage.getLastUsages().map((usage) => ({
20+
name: usage.name,
21+
last_used: usage.lastUsed,
22+
license_level: usage.licenseType,
23+
})),
24+
},
2725
});
2826
}
2927
);

x-pack/plugins/licensing/server/services/feature_usage_service.test.ts

Lines changed: 43 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,13 @@ describe('FeatureUsageService', () => {
1717
jest.restoreAllMocks();
1818
});
1919

20-
const toObj = (map: ReadonlyMap<string, any>): Record<string, any> =>
21-
Object.fromEntries(map.entries());
22-
2320
describe('#setup', () => {
2421
describe('#register', () => {
2522
it('throws when registering the same feature twice', () => {
2623
const setup = service.setup();
27-
setup.register('foo');
24+
setup.register('foo', 'basic');
2825
expect(() => {
29-
setup.register('foo');
26+
setup.register('foo', 'basic');
3027
}).toThrowErrorMatchingInlineSnapshot(`"Feature 'foo' has already been registered."`);
3128
});
3229
});
@@ -36,32 +33,50 @@ describe('FeatureUsageService', () => {
3633
describe('#notifyUsage', () => {
3734
it('allows to notify a feature usage', () => {
3835
const setup = service.setup();
39-
setup.register('feature');
36+
setup.register('feature', 'basic');
4037
const start = service.start();
4138
start.notifyUsage('feature', 127001);
4239

43-
expect(start.getLastUsages().get('feature')).toBe(127001);
40+
expect(start.getLastUsages()).toEqual([
41+
{
42+
lastUsed: new Date(127001),
43+
licenseType: 'basic',
44+
name: 'feature',
45+
},
46+
]);
4447
});
4548

4649
it('can receive a Date object', () => {
4750
const setup = service.setup();
48-
setup.register('feature');
51+
setup.register('feature', 'basic');
4952
const start = service.start();
5053

5154
const usageTime = new Date(2015, 9, 21, 17, 54, 12);
5255
start.notifyUsage('feature', usageTime);
53-
expect(start.getLastUsages().get('feature')).toBe(usageTime.getTime());
56+
expect(start.getLastUsages()).toEqual([
57+
{
58+
lastUsed: usageTime,
59+
licenseType: 'basic',
60+
name: 'feature',
61+
},
62+
]);
5463
});
5564

5665
it('uses the current time when `usedAt` is unspecified', () => {
5766
jest.spyOn(Date, 'now').mockReturnValue(42);
5867

5968
const setup = service.setup();
60-
setup.register('feature');
69+
setup.register('feature', 'basic');
6170
const start = service.start();
6271
start.notifyUsage('feature');
6372

64-
expect(start.getLastUsages().get('feature')).toBe(42);
73+
expect(start.getLastUsages()).toEqual([
74+
{
75+
lastUsed: new Date(42),
76+
licenseType: 'basic',
77+
name: 'feature',
78+
},
79+
]);
6580
});
6681

6782
it('throws when notifying for an unregistered feature', () => {
@@ -76,40 +91,41 @@ describe('FeatureUsageService', () => {
7691
describe('#getLastUsages', () => {
7792
it('returns the last usage for all used features', () => {
7893
const setup = service.setup();
79-
setup.register('featureA');
80-
setup.register('featureB');
94+
setup.register('featureA', 'basic');
95+
setup.register('featureB', 'gold');
8196
const start = service.start();
8297
start.notifyUsage('featureA', 127001);
8398
start.notifyUsage('featureB', 6666);
8499

85-
expect(toObj(start.getLastUsages())).toEqual({
86-
featureA: 127001,
87-
featureB: 6666,
88-
});
100+
expect(start.getLastUsages()).toEqual([
101+
{ lastUsed: new Date(127001), licenseType: 'basic', name: 'featureA' },
102+
{ lastUsed: new Date(6666), licenseType: 'gold', name: 'featureB' },
103+
]);
89104
});
90105

91106
it('returns the last usage even after notifying for an older usage', () => {
92107
const setup = service.setup();
93-
setup.register('featureA');
108+
setup.register('featureA', 'basic');
94109
const start = service.start();
95110
start.notifyUsage('featureA', 1000);
96111
start.notifyUsage('featureA', 500);
97112

98-
expect(toObj(start.getLastUsages())).toEqual({
99-
featureA: 1000,
100-
});
113+
expect(start.getLastUsages()).toEqual([
114+
{ lastUsed: new Date(1000), licenseType: 'basic', name: 'featureA' },
115+
]);
101116
});
102117

103-
it('does not return entries for unused registered features', () => {
118+
it('returns entries for unused registered features', () => {
104119
const setup = service.setup();
105-
setup.register('featureA');
106-
setup.register('featureB');
120+
setup.register('featureA', 'basic');
121+
setup.register('featureB', 'gold');
107122
const start = service.start();
108123
start.notifyUsage('featureA', 127001);
109124

110-
expect(toObj(start.getLastUsages())).toEqual({
111-
featureA: 127001,
112-
});
125+
expect(start.getLastUsages()).toEqual([
126+
{ lastUsed: new Date(127001), licenseType: 'basic', name: 'featureA' },
127+
{ lastUsed: null, licenseType: 'gold', name: 'featureB' },
128+
]);
113129
});
114130
});
115131
});

x-pack/plugins/licensing/server/services/feature_usage_service.ts

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,20 @@
55
*/
66

77
import { isDate } from 'lodash';
8+
import { LicenseType } from '../../common/types';
89

910
/** @public */
1011
export interface FeatureUsageServiceSetup {
1112
/**
1213
* Register a feature to be able to notify of it's usages using the {@link FeatureUsageServiceStart | service start contract}.
1314
*/
14-
register(featureName: string): void;
15+
register(featureName: string, licenseType: LicenseType): void;
16+
}
17+
18+
export interface LastFeatureUsage {
19+
name: string;
20+
lastUsed: Date | null;
21+
licenseType: LicenseType;
1522
}
1623

1724
/** @public */
@@ -27,37 +34,41 @@ export interface FeatureUsageServiceStart {
2734
* Return a map containing last usage timestamp for all features.
2835
* Features that were not used yet do not appear in the map.
2936
*/
30-
getLastUsages(): ReadonlyMap<string, number>;
37+
getLastUsages(): LastFeatureUsage[];
3138
}
3239

3340
export class FeatureUsageService {
34-
private readonly features: string[] = [];
35-
private readonly lastUsages = new Map<string, number>();
41+
private readonly lastUsages = new Map<string, LastFeatureUsage>();
3642

3743
public setup(): FeatureUsageServiceSetup {
3844
return {
39-
register: (featureName) => {
40-
if (this.features.includes(featureName)) {
45+
register: (featureName, licenseType) => {
46+
if (this.lastUsages.has(featureName)) {
4147
throw new Error(`Feature '${featureName}' has already been registered.`);
4248
}
43-
this.features.push(featureName);
49+
this.lastUsages.set(featureName, {
50+
name: featureName,
51+
lastUsed: null,
52+
licenseType,
53+
});
4454
},
4555
};
4656
}
4757

4858
public start(): FeatureUsageServiceStart {
4959
return {
5060
notifyUsage: (featureName, usedAt = Date.now()) => {
51-
if (!this.features.includes(featureName)) {
61+
const usage = this.lastUsages.get(featureName);
62+
if (!usage) {
5263
throw new Error(`Feature '${featureName}' is not registered.`);
5364
}
54-
if (isDate(usedAt)) {
55-
usedAt = usedAt.getTime();
65+
66+
const lastUsed = isDate(usedAt) ? usedAt : new Date(usedAt);
67+
if (usage.lastUsed == null || lastUsed > usage.lastUsed) {
68+
usage.lastUsed = lastUsed;
5669
}
57-
const currentValue = this.lastUsages.get(featureName) ?? 0;
58-
this.lastUsages.set(featureName, Math.max(usedAt, currentValue));
5970
},
60-
getLastUsages: () => new Map(this.lastUsages.entries()),
71+
getLastUsages: () => Array.from(this.lastUsages.values()),
6172
};
6273
}
6374
}

x-pack/test/plugin_api_integration/plugins/feature_usage_test/server/plugin.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,9 @@ export class FeatureUsageTestPlugin
3838
}: CoreSetup<FeatureUsageTestStartDependencies, FeatureUsageTestPluginStart>,
3939
{ licensing }: FeatureUsageTestSetupDependencies
4040
) {
41-
licensing.featureUsage.register('test_feature_a');
42-
licensing.featureUsage.register('test_feature_b');
43-
licensing.featureUsage.register('test_feature_c');
41+
licensing.featureUsage.register('Test feature A', 'basic');
42+
licensing.featureUsage.register('Test feature B', 'gold');
43+
licensing.featureUsage.register('Test feature C', 'platinum');
4444

4545
registerRoutes(http.createRouter(), getStartServices);
4646

x-pack/test/plugin_api_integration/test_suites/licensed_feature_usage/feature_usage.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,32 @@ export default function ({ getService }: FtrProviderContext) {
2020
describe('/api/licensing/feature_usage', () => {
2121
it('returns a map of last feature usages', async () => {
2222
const timeA = Date.now();
23-
await notifyUsage('test_feature_a', timeA);
23+
await notifyUsage('Test feature C', timeA);
2424

2525
const timeB = Date.now() - 4567;
26-
await notifyUsage('test_feature_b', timeB);
26+
await notifyUsage('Test feature B', timeB);
2727

2828
const response = await supertest.get('/api/licensing/feature_usage').expect(200);
2929

30-
expect(response.body.test_feature_a).to.eql(toISO(timeA));
31-
expect(response.body.test_feature_b).to.eql(toISO(timeB));
30+
expect(response.body).to.eql({
31+
features: [
32+
{
33+
last_used: null,
34+
license_level: 'basic',
35+
name: 'Test feature A',
36+
},
37+
{
38+
last_used: toISO(timeB),
39+
license_level: 'gold',
40+
name: 'Test feature B',
41+
},
42+
{
43+
last_used: toISO(timeA),
44+
license_level: 'platinum',
45+
name: 'Test feature C',
46+
},
47+
],
48+
});
3249
});
3350
});
3451
}

0 commit comments

Comments
 (0)