Skip to content

Commit

Permalink
Updating the licensed feature usage API response format (#67712)
Browse files Browse the repository at this point in the history
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
  • Loading branch information
kobelb and elasticmachine authored Jun 1, 2020
1 parent daf26b9 commit ce47ef5
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 56 deletions.
16 changes: 7 additions & 9 deletions x-pack/plugins/licensing/server/routes/feature_usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,13 @@ export function registerFeatureUsageRoute(
async (context, request, response) => {
const [, , { featureUsage }] = await getStartServices();
return response.ok({
body: [...featureUsage.getLastUsages().entries()].reduce(
(res, [featureName, lastUsage]) => {
return {
...res,
[featureName]: new Date(lastUsage).toISOString(),
};
},
{}
),
body: {
features: featureUsage.getLastUsages().map((usage) => ({
name: usage.name,
last_used: usage.lastUsed,
license_level: usage.licenseType,
})),
},
});
}
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,13 @@ describe('FeatureUsageService', () => {
jest.restoreAllMocks();
});

const toObj = (map: ReadonlyMap<string, any>): Record<string, any> =>
Object.fromEntries(map.entries());

describe('#setup', () => {
describe('#register', () => {
it('throws when registering the same feature twice', () => {
const setup = service.setup();
setup.register('foo');
setup.register('foo', 'basic');
expect(() => {
setup.register('foo');
setup.register('foo', 'basic');
}).toThrowErrorMatchingInlineSnapshot(`"Feature 'foo' has already been registered."`);
});
});
Expand All @@ -36,32 +33,50 @@ describe('FeatureUsageService', () => {
describe('#notifyUsage', () => {
it('allows to notify a feature usage', () => {
const setup = service.setup();
setup.register('feature');
setup.register('feature', 'basic');
const start = service.start();
start.notifyUsage('feature', 127001);

expect(start.getLastUsages().get('feature')).toBe(127001);
expect(start.getLastUsages()).toEqual([
{
lastUsed: new Date(127001),
licenseType: 'basic',
name: 'feature',
},
]);
});

it('can receive a Date object', () => {
const setup = service.setup();
setup.register('feature');
setup.register('feature', 'basic');
const start = service.start();

const usageTime = new Date(2015, 9, 21, 17, 54, 12);
start.notifyUsage('feature', usageTime);
expect(start.getLastUsages().get('feature')).toBe(usageTime.getTime());
expect(start.getLastUsages()).toEqual([
{
lastUsed: usageTime,
licenseType: 'basic',
name: 'feature',
},
]);
});

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

const setup = service.setup();
setup.register('feature');
setup.register('feature', 'basic');
const start = service.start();
start.notifyUsage('feature');

expect(start.getLastUsages().get('feature')).toBe(42);
expect(start.getLastUsages()).toEqual([
{
lastUsed: new Date(42),
licenseType: 'basic',
name: 'feature',
},
]);
});

it('throws when notifying for an unregistered feature', () => {
Expand All @@ -76,40 +91,41 @@ describe('FeatureUsageService', () => {
describe('#getLastUsages', () => {
it('returns the last usage for all used features', () => {
const setup = service.setup();
setup.register('featureA');
setup.register('featureB');
setup.register('featureA', 'basic');
setup.register('featureB', 'gold');
const start = service.start();
start.notifyUsage('featureA', 127001);
start.notifyUsage('featureB', 6666);

expect(toObj(start.getLastUsages())).toEqual({
featureA: 127001,
featureB: 6666,
});
expect(start.getLastUsages()).toEqual([
{ lastUsed: new Date(127001), licenseType: 'basic', name: 'featureA' },
{ lastUsed: new Date(6666), licenseType: 'gold', name: 'featureB' },
]);
});

it('returns the last usage even after notifying for an older usage', () => {
const setup = service.setup();
setup.register('featureA');
setup.register('featureA', 'basic');
const start = service.start();
start.notifyUsage('featureA', 1000);
start.notifyUsage('featureA', 500);

expect(toObj(start.getLastUsages())).toEqual({
featureA: 1000,
});
expect(start.getLastUsages()).toEqual([
{ lastUsed: new Date(1000), licenseType: 'basic', name: 'featureA' },
]);
});

it('does not return entries for unused registered features', () => {
it('returns entries for unused registered features', () => {
const setup = service.setup();
setup.register('featureA');
setup.register('featureB');
setup.register('featureA', 'basic');
setup.register('featureB', 'gold');
const start = service.start();
start.notifyUsage('featureA', 127001);

expect(toObj(start.getLastUsages())).toEqual({
featureA: 127001,
});
expect(start.getLastUsages()).toEqual([
{ lastUsed: new Date(127001), licenseType: 'basic', name: 'featureA' },
{ lastUsed: null, licenseType: 'gold', name: 'featureB' },
]);
});
});
});
Expand Down
37 changes: 24 additions & 13 deletions x-pack/plugins/licensing/server/services/feature_usage_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,20 @@
*/

import { isDate } from 'lodash';
import { LicenseType } from '../../common/types';

/** @public */
export interface FeatureUsageServiceSetup {
/**
* Register a feature to be able to notify of it's usages using the {@link FeatureUsageServiceStart | service start contract}.
*/
register(featureName: string): void;
register(featureName: string, licenseType: LicenseType): void;
}

export interface LastFeatureUsage {
name: string;
lastUsed: Date | null;
licenseType: LicenseType;
}

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

export class FeatureUsageService {
private readonly features: string[] = [];
private readonly lastUsages = new Map<string, number>();
private readonly lastUsages = new Map<string, LastFeatureUsage>();

public setup(): FeatureUsageServiceSetup {
return {
register: (featureName) => {
if (this.features.includes(featureName)) {
register: (featureName, licenseType) => {
if (this.lastUsages.has(featureName)) {
throw new Error(`Feature '${featureName}' has already been registered.`);
}
this.features.push(featureName);
this.lastUsages.set(featureName, {
name: featureName,
lastUsed: null,
licenseType,
});
},
};
}

public start(): FeatureUsageServiceStart {
return {
notifyUsage: (featureName, usedAt = Date.now()) => {
if (!this.features.includes(featureName)) {
const usage = this.lastUsages.get(featureName);
if (!usage) {
throw new Error(`Feature '${featureName}' is not registered.`);
}
if (isDate(usedAt)) {
usedAt = usedAt.getTime();

const lastUsed = isDate(usedAt) ? usedAt : new Date(usedAt);
if (usage.lastUsed == null || lastUsed > usage.lastUsed) {
usage.lastUsed = lastUsed;
}
const currentValue = this.lastUsages.get(featureName) ?? 0;
this.lastUsages.set(featureName, Math.max(usedAt, currentValue));
},
getLastUsages: () => new Map(this.lastUsages.entries()),
getLastUsages: () => Array.from(this.lastUsages.values()),
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ export class FeatureUsageTestPlugin
}: CoreSetup<FeatureUsageTestStartDependencies, FeatureUsageTestPluginStart>,
{ licensing }: FeatureUsageTestSetupDependencies
) {
licensing.featureUsage.register('test_feature_a');
licensing.featureUsage.register('test_feature_b');
licensing.featureUsage.register('test_feature_c');
licensing.featureUsage.register('Test feature A', 'basic');
licensing.featureUsage.register('Test feature B', 'gold');
licensing.featureUsage.register('Test feature C', 'platinum');

registerRoutes(http.createRouter(), getStartServices);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,32 @@ export default function ({ getService }: FtrProviderContext) {
describe('/api/licensing/feature_usage', () => {
it('returns a map of last feature usages', async () => {
const timeA = Date.now();
await notifyUsage('test_feature_a', timeA);
await notifyUsage('Test feature C', timeA);

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

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

expect(response.body.test_feature_a).to.eql(toISO(timeA));
expect(response.body.test_feature_b).to.eql(toISO(timeB));
expect(response.body).to.eql({
features: [
{
last_used: null,
license_level: 'basic',
name: 'Test feature A',
},
{
last_used: toISO(timeB),
license_level: 'gold',
name: 'Test feature B',
},
{
last_used: toISO(timeA),
license_level: 'platinum',
name: 'Test feature C',
},
],
});
});
});
}

0 comments on commit ce47ef5

Please sign in to comment.