Skip to content

Commit

Permalink
feat: expose lifecycle stage in project overview search (#7017)
Browse files Browse the repository at this point in the history
  • Loading branch information
kwasniew committed May 9, 2024
1 parent 28a7797 commit 97d702a
Show file tree
Hide file tree
Showing 8 changed files with 102 additions and 5 deletions.
9 changes: 8 additions & 1 deletion src/lib/db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { InactiveUsersStore } from '../users/inactive/inactive-users-store';
import { TrafficDataUsageStore } from '../features/traffic-data-usage/traffic-data-usage-store';
import { SegmentReadModel } from '../features/segment/segment-read-model';
import { ProjectOwnersReadModel } from '../features/project/project-owners-read-model';
import { FeatureLifecycleStore } from '../features/feature-lifecycle/feature-lifecycle-store';

export const createStores = (
config: IUnleashConfig,
Expand Down Expand Up @@ -145,11 +146,17 @@ export const createStores = (
privateProjectStore: new PrivateProjectStore(db, getLogger),
dependentFeaturesStore: new DependentFeaturesStore(db),
lastSeenStore: new LastSeenStore(db, eventBus, getLogger),
featureSearchStore: new FeatureSearchStore(db, eventBus, getLogger),
featureSearchStore: new FeatureSearchStore(
db,
eventBus,
getLogger,
config.flagResolver,
),
inactiveUsersStore: new InactiveUsersStore(db, eventBus, getLogger),
trafficDataUsageStore: new TrafficDataUsageStore(db, getLogger),
segmentReadModel: new SegmentReadModel(db),
projectOwnersReadModel: new ProjectOwnersReadModel(db),
featureLifecycleStore: new FeatureLifecycleStore(db),
};
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const createFeatureSearchService =
db,
eventBus,
getLogger,
flagResolver,
);

return new FeatureSearchService(
Expand Down
54 changes: 51 additions & 3 deletions src/lib/features/feature-search/feature-search-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
IFeatureOverview,
IFeatureSearchOverview,
IFeatureSearchStore,
IFlagResolver,
ITag,
} from '../../types';
import FeatureToggleStore from '../feature-toggle/feature-toggle-store';
Expand Down Expand Up @@ -40,9 +41,17 @@ class FeatureSearchStore implements IFeatureSearchStore {

private readonly timer: Function;

constructor(db: Db, eventBus: EventEmitter, getLogger: LogProvider) {
private flagResolver: IFlagResolver;

constructor(
db: Db,
eventBus: EventEmitter,
getLogger: LogProvider,
flagResolver: IFlagResolver,
) {
this.db = db;
this.logger = getLogger('feature-search-store.ts');
this.flagResolver = flagResolver;
this.timer = (action) =>
metricsHelper.wrapTimer(eventBus, DB_TIME, {
store: 'feature-search',
Expand All @@ -65,6 +74,20 @@ class FeatureSearchStore implements IFeatureSearchStore {
};
}

private getLatestLifecycleStageQuery() {
return this.db('feature_lifecycles')
.select(
'feature as stage_feature',
'stage as latest_stage',
'created_at as entered_stage_at',
)
.distinctOn('stage_feature')
.orderBy([
'stage_feature',
{ column: 'entered_stage_at', order: 'desc' },
]);
}

async searchFeatures(
{
userId,
Expand All @@ -86,6 +109,9 @@ class FeatureSearchStore implements IFeatureSearchStore {
const validatedSortOrder =
sortOrder === 'asc' || sortOrder === 'desc' ? sortOrder : 'asc';

const featureLifecycleEnabled =
this.flagResolver.isEnabled('featureLifecycle');

const finalQuery = this.db
.with('ranked_features', (query) => {
query.from('features');
Expand Down Expand Up @@ -235,6 +261,7 @@ class FeatureSearchStore implements IFeatureSearchStore {
.select(selectColumns)
.denseRank('rank', this.db.raw(rankingSql));
})
.with('lifecycle', this.getLatestLifecycleStageQuery())
.with(
'final_ranks',
this.db.raw(
Expand Down Expand Up @@ -290,10 +317,20 @@ class FeatureSearchStore implements IFeatureSearchStore {
.joinRaw('CROSS JOIN total_features')
.whereBetween('final_rank', [offset + 1, offset + limit])
.orderBy('final_rank');
if (featureLifecycleEnabled) {
finalQuery.leftJoin(
'lifecycle',
'ranked_features.feature_name',
'lifecycle.stage_feature',
);
}
const rows = await finalQuery;
stopTimer();
if (rows.length > 0) {
const overview = this.getAggregatedSearchData(rows);
const overview = this.getAggregatedSearchData(
rows,
featureLifecycleEnabled,
);
const features = sortEnvironments(overview);
return {
features,
Expand Down Expand Up @@ -349,7 +386,10 @@ class FeatureSearchStore implements IFeatureSearchStore {
return rankingSql;
}

getAggregatedSearchData(rows): IFeatureSearchOverview[] {
getAggregatedSearchData(
rows,
featureLifecycleEnabled: boolean,
): IFeatureSearchOverview[] {
const entriesMap: Map<string, IFeatureSearchOverview> = new Map();
const orderedEntries: IFeatureSearchOverview[] = [];

Expand All @@ -372,6 +412,14 @@ class FeatureSearchStore implements IFeatureSearchStore {
environments: [],
segments: row.segment_name ? [row.segment_name] : [],
};
if (featureLifecycleEnabled) {
entry.lifecycle = row.latest_stage
? {
stage: row.latest_stage,
enteredStageAt: row.entered_stage_at,
}
: undefined;
}
entriesMap.set(row.feature_name, entry);
orderedEntries.push(entry);
}
Expand Down
11 changes: 10 additions & 1 deletion src/lib/features/feature-search/feature.search.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ beforeAll(async () => {
experimental: {
flags: {
strictSchemaValidation: true,
featureLifecycle: true,
},
},
},
Expand Down Expand Up @@ -924,7 +925,7 @@ test('should filter features by combined operators', async () => {
});
});

test('should return environment usage metrics', async () => {
test('should return environment usage metrics and lifecycle', async () => {
await app.createFeature({
name: 'my_feature_b',
createdAt: '2023-01-29T15:21:39.975Z',
Expand Down Expand Up @@ -957,13 +958,21 @@ test('should return environment usage metrics', async () => {
},
]);

await stores.featureLifecycleStore.insert([
{ feature: 'my_feature_b', stage: 'initial' },
]);
await stores.featureLifecycleStore.insert([
{ feature: 'my_feature_b', stage: 'pre-live' },
]);

const { body } = await searchFeatures({
query: 'my_feature_b',
});
expect(body).toMatchObject({
features: [
{
name: 'my_feature_b',
lifecycle: { stage: 'pre-live' },
environments: [
{
name: 'default',
Expand Down
26 changes: 26 additions & 0 deletions src/lib/openapi/spec/feature-search-response-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,32 @@ export const featureSearchResponseSchema = {
nullable: true,
description: 'The list of feature tags',
},
lifecycle: {
type: 'object',
description: 'Current lifecycle stage of the feature',
additionalProperties: false,
required: ['stage', 'enteredStageAt'],
properties: {
stage: {
description: 'The name of the current lifecycle stage',
type: 'string',
enum: [
'initial',
'pre-live',
'live',
'completed',
'archived',
],
example: 'initial',
},
enteredStageAt: {
description: 'When the feature entered this stage',
type: 'string',
format: 'date-time',
example: '2023-01-28T15:21:39.975Z',
},
},
},
},
components: {
schemas: {
Expand Down
1 change: 1 addition & 0 deletions src/lib/types/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ export interface IFeatureOverview {
createdAt: Date;
lastSeenAt: Date;
environments: IEnvironmentOverview[];
lifecycle?: IFeatureLifecycleStage;
}

export type IFeatureSearchOverview = Exclude<
Expand Down
3 changes: 3 additions & 0 deletions src/lib/types/stores.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import type { IInactiveUsersStore } from '../users/inactive/types/inactive-users
import { ITrafficDataUsageStore } from '../features/traffic-data-usage/traffic-data-usage-store-type';
import { ISegmentReadModel } from '../features/segment/segment-read-model-type';
import { IProjectOwnersReadModel } from '../features/project/project-owners-read-model.type';
import { IFeatureLifecycleStore } from '../features/feature-lifecycle/feature-lifecycle-store-type';

export interface IUnleashStores {
accessStore: IAccessStore;
Expand Down Expand Up @@ -86,6 +87,7 @@ export interface IUnleashStores {
trafficDataUsageStore: ITrafficDataUsageStore;
segmentReadModel: ISegmentReadModel;
projectOwnersReadModel: IProjectOwnersReadModel;
featureLifecycleStore: IFeatureLifecycleStore;
}

export {
Expand Down Expand Up @@ -130,4 +132,5 @@ export {
ITrafficDataUsageStore,
ISegmentReadModel,
IProjectOwnersReadModel,
IFeatureLifecycleStore,
};
2 changes: 2 additions & 0 deletions src/test/fixtures/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { FakeInactiveUsersStore } from '../../lib/users/inactive/fakes/fake-inac
import { FakeTrafficDataUsageStore } from '../../lib/features/traffic-data-usage/fake-traffic-data-usage-store';
import { FakeSegmentReadModel } from '../../lib/features/segment/fake-segment-read-model';
import { FakeProjectOwnersReadModel } from '../../lib/features/project/fake-project-owners-read-model';
import { FakeFeatureLifecycleStore } from '../../lib/features/feature-lifecycle/fake-feature-lifecycle-store';

const db = {
select: () => ({
Expand Down Expand Up @@ -97,6 +98,7 @@ const createStores: () => IUnleashStores = () => {
trafficDataUsageStore: new FakeTrafficDataUsageStore(),
segmentReadModel: new FakeSegmentReadModel(),
projectOwnersReadModel: new FakeProjectOwnersReadModel(),
featureLifecycleStore: new FakeFeatureLifecycleStore(),
};
};

Expand Down

0 comments on commit 97d702a

Please sign in to comment.