From 16e3799b9a399d79b2a025eaa4b2d5957a012349 Mon Sep 17 00:00:00 2001 From: andreas-unleash Date: Fri, 14 Jul 2023 04:46:13 +0300 Subject: [PATCH] Feat/add strategy update event on strategy ordering (#4234) Adds a `feature-strategy-update-event` when the strategy sort-order is changed. Makes all fields in the eventDataSchema nullable Closes # [1-11120](https://linear.app/unleash/issue/1-1112/we-should-have-event-for-re-ordering-strategies) --------- Signed-off-by: andreas-unleash --- src/lib/openapi/spec/event-schema.ts | 16 +++- .../admin-api/project/project-features.ts | 18 +++-- src/lib/services/feature-toggle-service.ts | 75 +++++++++++++++---- .../api/admin/project/features.e2e.test.ts | 67 +++++++++++++++++ 4 files changed, 155 insertions(+), 21 deletions(-) diff --git a/src/lib/openapi/spec/event-schema.ts b/src/lib/openapi/spec/event-schema.ts index 9dd6cac18e..5af07bde2d 100644 --- a/src/lib/openapi/spec/event-schema.ts +++ b/src/lib/openapi/spec/event-schema.ts @@ -12,29 +12,35 @@ const eventDataSchema = { description: 'Name of the feature toggle/strategy/environment that this event relates to', example: 'my.first.toggle', + nullable: true, }, description: { type: 'string', description: 'The description of the object this event relates to', example: 'Toggle description', + nullable: true, }, type: { type: 'string', + nullable: true, description: 'If this event relates to a feature toggle, the type of feature toggle.', example: 'release', }, project: { + nullable: true, type: 'string', description: 'The project this event relates to', example: 'default', }, stale: { + nullable: true, description: 'Is the feature toggle this event relates to stale', type: 'boolean', example: true, }, variants: { + nullable: true, description: 'Variants configured for this toggle', type: 'array', items: { @@ -42,6 +48,7 @@ const eventDataSchema = { }, }, createdAt: { + nullable: true, type: 'string', format: 'date-time', description: @@ -49,11 +56,11 @@ const eventDataSchema = { example: '2023-07-05T12:56:00.000Z', }, lastSeenAt: { + nullable: true, type: 'string', format: 'date-time', description: 'The time the feature was last seen', example: '2023-07-05T12:56:00.000Z', - nullable: true, }, impressionData: { description: @@ -127,7 +134,12 @@ export const eventSchema = { 'The name of the feature toggle the event relates to, if applicable.', example: 'my.first.feature', }, - data: eventDataSchema, + data: { + ...eventDataSchema, + description: + "Data relating to the current state of the event's subject.", + nullable: true, + }, preData: { ...eventDataSchema, description: diff --git a/src/lib/routes/admin-api/project/project-features.ts b/src/lib/routes/admin-api/project/project-features.ts index f342330225..0d933dcfe1 100644 --- a/src/lib/routes/admin-api/project/project-features.ts +++ b/src/lib/routes/admin-api/project/project-features.ts @@ -936,7 +936,7 @@ export default class ProjectFeaturesController extends Controller { } async setStrategiesSortOrder( - req: Request< + req: IAuthRequest< FeatureStrategyParams, any, SetStrategySortOrderSchema, @@ -944,10 +944,18 @@ export default class ProjectFeaturesController extends Controller { >, res: Response, ): Promise { - const { featureName } = req.params; - await this.featureService.updateStrategiesSortOrder( - featureName, - req.body, + const { featureName, projectId: project, environment } = req.params; + const createdBy = extractUsername(req); + await this.startTransaction(async (tx) => + this.transactionalFeatureToggleService( + tx, + ).updateStrategiesSortOrder( + featureName, + environment, + project, + createdBy, + req.body, + ), ); res.status(200).send(); diff --git a/src/lib/services/feature-toggle-service.ts b/src/lib/services/feature-toggle-service.ts index 987542c13b..905ebfca75 100644 --- a/src/lib/services/feature-toggle-service.ts +++ b/src/lib/services/feature-toggle-service.ts @@ -369,7 +369,7 @@ class FeatureToggleService { featureStrategy: IFeatureStrategy, segments: ISegment[] = [], ): Saved { - return { + const result: Saved = { id: featureStrategy.id, name: featureStrategy.strategyName, title: featureStrategy.title, @@ -378,16 +378,56 @@ class FeatureToggleService { parameters: featureStrategy.parameters, segments: segments.map((segment) => segment.id) ?? [], }; + + if (this.flagResolver.isEnabled('strategyVariant')) { + result.sortOrder = featureStrategy.sortOrder; + } + return result; } async updateStrategiesSortOrder( featureName: string, + environment: string, + project: string, + createdBy: string, sortOrders: SetStrategySortOrderSchema, ): Promise> { await Promise.all( - sortOrders.map(async ({ id, sortOrder }) => - this.featureStrategiesStore.updateSortOrder(id, sortOrder), - ), + sortOrders.map(async ({ id, sortOrder }) => { + const strategyToUpdate = + await this.featureStrategiesStore.getStrategyById(id); + await this.featureStrategiesStore.updateSortOrder( + id, + sortOrder, + ); + const updatedStrategy = + await this.featureStrategiesStore.getStrategyById(id); + + const tags = await this.tagStore.getAllTagsForFeature( + featureName, + ); + const segments = await this.segmentService.getByStrategy( + strategyToUpdate.id, + ); + const strategy = this.featureStrategyToPublic( + updatedStrategy, + segments, + ); + await this.eventStore.store( + new FeatureStrategyUpdateEvent({ + featureName, + environment, + project, + createdBy, + preData: this.featureStrategyToPublic( + strategyToUpdate, + segments, + ), + data: strategy, + tags: tags, + }), + ); + }), ); } @@ -472,24 +512,31 @@ class FeatureToggleService { ); } - const tags = await this.tagStore.getAllTagsForFeature(featureName); const segments = await this.segmentService.getByStrategy( newFeatureStrategy.id, ); + const strategy = this.featureStrategyToPublic( newFeatureStrategy, segments, ); - await this.eventStore.store( - new FeatureStrategyAddEvent({ - project: projectId, + + if (this.flagResolver.isEnabled('strategyVariant')) { + const tags = await this.tagStore.getAllTagsForFeature( featureName, - createdBy, - environment, - data: strategy, - tags, - }), - ); + ); + + await this.eventStore.store( + new FeatureStrategyAddEvent({ + project: projectId, + featureName, + createdBy, + environment, + data: strategy, + tags, + }), + ); + } return strategy; } catch (e) { if (e.code === FOREIGN_KEY_VIOLATION) { diff --git a/src/test/e2e/api/admin/project/features.e2e.test.ts b/src/test/e2e/api/admin/project/features.e2e.test.ts index 441c138a30..7ef392ae88 100644 --- a/src/test/e2e/api/admin/project/features.e2e.test.ts +++ b/src/test/e2e/api/admin/project/features.e2e.test.ts @@ -26,6 +26,10 @@ import { v4 as uuidv4 } from 'uuid'; import supertest from 'supertest'; import { randomId } from '../../../../../lib/util/random-id'; import { DEFAULT_PROJECT } from '../../../../../lib/types'; +import { + FeatureStrategySchema, + SetStrategySortOrderSchema, +} from '../../../../../lib/openapi'; let app: IUnleashTest; let db: ITestDb; @@ -3227,3 +3231,66 @@ test('Enabling a feature environment should add the default strategy when only d expect(res.body.strategies[1].disabled).toBeFalsy(); }); }); + +test('Updating feature strategy sort-order should trigger a FeatureStrategyUpdatedEvent when strategyVariant is true', async () => { + app = await setupAppWithCustomConfig( + db.stores, + { + experimental: { + flags: { + strictSchemaValidation: true, + strategyVariant: true, + }, + }, + }, + db.rawDatabase, + ); + + const envName = 'sort-order-within-environment-strategyVariant'; + const featureName = 'feature.sort.order.event.list'; + + await db.stores.environmentStore.create({ + name: envName, + type: 'test', + }); + + await app.request + .post('/api/admin/projects/default/environments') + .send({ + environment: envName, + }) + .expect(200); + + await app.request + .post('/api/admin/projects/default/features') + .send({ name: featureName }) + .expect(201); + + await addStrategies(featureName, envName); + const { body } = await app.request.get( + `/api/admin/projects/default/features/${featureName}/environments/${envName}/strategies`, + ); + + const strategies: FeatureStrategySchema[] = body; + let order = 1; + const sortOrders: SetStrategySortOrderSchema = []; + + strategies.forEach((strategy) => { + sortOrders.push({ id: strategy.id!, sortOrder: order++ }); + }); + + await app.request + .post( + `/api/admin/projects/default/features/${featureName}/environments/${envName}/strategies/set-sort-order`, + ) + .send(sortOrders) + .expect(200); + + const response = await app.request.get(`/api/admin/events`); + const { body: eventsBody } = response; + let { events } = eventsBody; + + expect(events[0].type).toBe('feature-strategy-update'); + expect(events[1].type).toBe('feature-strategy-update'); + expect(events[2].type).toBe('feature-strategy-update'); +});