Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: expose lifecycle stage in project overview search #7017

Merged
merged 3 commits into from
May 9, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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,
),
Comment on lines +149 to +154

Choose a reason for hiding this comment

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

❌ Getting worse: Large Method
createStores increases from 106 to 112 lines of code, threshold = 70

Suppress

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
45 changes: 44 additions & 1 deletion 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 Down Expand Up @@ -86,6 +95,19 @@ class FeatureSearchStore implements IFeatureSearchStore {
const validatedSortOrder =
sortOrder === 'asc' || sortOrder === 'desc' ? sortOrder : 'asc';

const featureLifecycleEnabled =
this.flagResolver.isEnabled('featureLifecycle');
const latestLifecycleStageQuery = this.db
Copy link
Contributor

Choose a reason for hiding this comment

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

Also can we look into opportunities to pull it out as a method, and put all selects, joins and query there

.select([
'feature_lifecycles.feature',
'feature_lifecycles.stage as latest_stage',
this.db.raw(
'MAX(feature_lifecycles.created_at) OVER (PARTITION BY feature_lifecycles.feature) as entered_stage_at',
),
])
.from('feature_lifecycles')
.as('lifecycle');

const finalQuery = this.db
.with('ranked_features', (query) => {
query.from('features');
Expand All @@ -112,6 +134,13 @@ class FeatureSearchStore implements IFeatureSearchStore {
const lastSeenQuery = 'last_seen_at_metrics.last_seen_at';
selectColumns.push(`${lastSeenQuery} as env_last_seen_at`);

if (featureLifecycleEnabled) {
selectColumns.push(
'lifecycle.latest_stage',
'lifecycle.entered_stage_at',
);
}

if (userId) {
query.leftJoin(`favorite_features`, function () {
this.on(
Expand Down Expand Up @@ -224,6 +253,14 @@ class FeatureSearchStore implements IFeatureSearchStore {
);
});

if (featureLifecycleEnabled) {
query.leftJoin(
Copy link
Contributor

Choose a reason for hiding this comment

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

I would not join it into original query, but rather into final query, where the paging has been done already.

latestLifecycleStageQuery,
'lifecycle.feature',
'features.name',
);
}

const rankingSql = this.buildRankingSql(
favoritesFirst,
sortBy,
Expand Down Expand Up @@ -371,6 +408,12 @@ class FeatureSearchStore implements IFeatureSearchStore {
dependencyType: row.dependency,
environments: [],
segments: row.segment_name ? [row.segment_name] : [],
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 @@ -236,6 +236,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