diff --git a/src/lib/features/feature-toggle/tests/feature-toggle-service.e2e.test.ts b/src/lib/features/feature-toggle/tests/feature-toggle-service.e2e.test.ts index 351470fbd1..114e8e0e75 100644 --- a/src/lib/features/feature-toggle/tests/feature-toggle-service.e2e.test.ts +++ b/src/lib/features/feature-toggle/tests/feature-toggle-service.e2e.test.ts @@ -18,11 +18,13 @@ import { insertLastSeenAt, insertFeatureEnvironmentsLastSeen, } from '../../../../test/e2e/helpers/test-helper'; +import { EventService } from '../../../services'; let stores: IUnleashStores; let db; let service: FeatureToggleService; let segmentService: ISegmentService; +let eventService: EventService; let environmentService: EnvironmentService; let unleashConfig; @@ -48,6 +50,8 @@ beforeAll(async () => { segmentService = createSegmentService(db.rawDatabase, config); service = createFeatureToggleService(db.rawDatabase, config); + + eventService = new EventService(stores, config); }); afterAll(async () => { @@ -253,13 +257,17 @@ test('adding and removing an environment preserves variants when variants per en ); //force the variantEnvironments flag off so that we can test legacy behavior - environmentService = new EnvironmentService(stores, { - ...unleashConfig, - flagResolver: { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - isEnabled: (toggleName: string) => false, + environmentService = new EnvironmentService( + stores, + { + ...unleashConfig, + flagResolver: { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + isEnabled: (toggleName: string) => false, + }, }, - }); + eventService, + ); await environmentService.addEnvironmentToProject(prodEnv, 'default'); await environmentService.removeEnvironmentFromProject(prodEnv, 'default'); diff --git a/src/lib/routes/admin-api/project/environments.ts b/src/lib/routes/admin-api/project/environments.ts index 30541c246a..03f4026297 100644 --- a/src/lib/routes/admin-api/project/environments.ts +++ b/src/lib/routes/admin-api/project/environments.ts @@ -18,6 +18,8 @@ import { ProjectEnvironmentSchema, } from '../../../openapi'; import { OpenApiService, ProjectService } from '../../../services'; +import { extractUsername } from '../../../util'; +import { IAuthRequest } from '../../unleash-types'; const PREFIX = '/:projectId/environments'; @@ -124,7 +126,7 @@ export default class EnvironmentsController extends Controller { } async addEnvironmentToProject( - req: Request< + req: IAuthRequest< Omit, void, ProjectEnvironmentSchema @@ -138,13 +140,14 @@ export default class EnvironmentsController extends Controller { await this.environmentService.addEnvironmentToProject( environment, projectId, + extractUsername(req), ); res.status(200).end(); } async removeEnvironmentFromProject( - req: Request, + req: IAuthRequest, res: Response, ): Promise { const { projectId, environment } = req.params; @@ -152,6 +155,7 @@ export default class EnvironmentsController extends Controller { await this.environmentService.removeEnvironmentFromProject( environment, projectId, + extractUsername(req), ); res.status(200).end(); diff --git a/src/lib/services/environment-service.ts b/src/lib/services/environment-service.ts index a2bf54ed85..4e9360b0a2 100644 --- a/src/lib/services/environment-service.ts +++ b/src/lib/services/environment-service.ts @@ -7,6 +7,8 @@ import { ISortOrder, IUnleashConfig, IUnleashStores, + PROJECT_ENVIRONMENT_ADDED, + PROJECT_ENVIRONMENT_REMOVED, } from '../types'; import { Logger } from '../logger'; import { BadDataError, UNIQUE_CONSTRAINT_VIOLATION } from '../error'; @@ -17,6 +19,7 @@ import { IProjectStore } from 'lib/types/stores/project-store'; import MinimumOneEnvironmentError from '../error/minimum-one-environment-error'; import { IFlagResolver } from 'lib/types/experimental'; import { CreateFeatureStrategySchema } from '../openapi'; +import EventService from './event-service'; export default class EnvironmentService { private logger: Logger; @@ -29,6 +32,8 @@ export default class EnvironmentService { private featureEnvironmentStore: IFeatureEnvironmentStore; + private eventService: EventService; + private flagResolver: IFlagResolver; constructor( @@ -48,12 +53,14 @@ export default class EnvironmentService { getLogger, flagResolver, }: Pick, + eventService: EventService, ) { this.logger = getLogger('services/environment-service.ts'); this.environmentStore = environmentStore; this.featureStrategiesStore = featureStrategiesStore; this.featureEnvironmentStore = featureEnvironmentStore; this.projectStore = projectStore; + this.eventService = eventService; this.flagResolver = flagResolver; } @@ -92,6 +99,7 @@ export default class EnvironmentService { async addEnvironmentToProject( environment: string, projectId: string, + username = 'unknown', ): Promise { try { await this.featureEnvironmentStore.connectProject( @@ -102,6 +110,12 @@ export default class EnvironmentService { environment, projectId, ); + await this.eventService.storeEvent({ + type: PROJECT_ENVIRONMENT_ADDED, + project: projectId, + environment, + createdBy: username, + }); } catch (e) { if (e.code === UNIQUE_CONSTRAINT_VIOLATION) { throw new NameExistsError( @@ -213,6 +227,7 @@ export default class EnvironmentService { async removeEnvironmentFromProject( environment: string, projectId: string, + username = 'unknown', ): Promise { const projectEnvs = await this.projectStore.getEnvironmentsForProject(projectId); @@ -222,6 +237,12 @@ export default class EnvironmentService { environment, projectId, ); + await this.eventService.storeEvent({ + type: PROJECT_ENVIRONMENT_REMOVED, + project: projectId, + environment, + createdBy: username, + }); return; } throw new MinimumOneEnvironmentError( diff --git a/src/lib/services/index.ts b/src/lib/services/index.ts index 02634c7656..6f4d203207 100644 --- a/src/lib/services/index.ts +++ b/src/lib/services/index.ts @@ -222,7 +222,11 @@ export const createServices = ( dependentFeaturesReadModel, dependentFeaturesService, ); - const environmentService = new EnvironmentService(stores, config); + const environmentService = new EnvironmentService( + stores, + config, + eventService, + ); const featureTagService = new FeatureTagService( stores, config, diff --git a/src/lib/types/events.ts b/src/lib/types/events.ts index 35ba29b1ad..4b8ac69dfc 100644 --- a/src/lib/types/events.ts +++ b/src/lib/types/events.ts @@ -108,6 +108,9 @@ export const GROUP_DELETED = 'group-deleted' as const; export const SETTING_CREATED = 'setting-created' as const; export const SETTING_UPDATED = 'setting-updated' as const; export const SETTING_DELETED = 'setting-deleted' as const; +export const PROJECT_ENVIRONMENT_ADDED = 'project-environment-added' as const; +export const PROJECT_ENVIRONMENT_REMOVED = + 'project-environment-removed' as const; export const CLIENT_METRICS = 'client-metrics' as const; export const CLIENT_REGISTER = 'client-register' as const; @@ -292,6 +295,8 @@ export const IEventTypes = [ BANNER_CREATED, BANNER_UPDATED, BANNER_DELETED, + PROJECT_ENVIRONMENT_ADDED, + PROJECT_ENVIRONMENT_REMOVED, ] as const; export type IEventType = (typeof IEventTypes)[number]; diff --git a/src/test/e2e/services/environment-service.test.ts b/src/test/e2e/services/environment-service.test.ts index d0463188b3..b214f3cfb5 100644 --- a/src/test/e2e/services/environment-service.test.ts +++ b/src/test/e2e/services/environment-service.test.ts @@ -4,16 +4,19 @@ import dbInit from '../helpers/database-init'; import NotFoundError from '../../../lib/error/notfound-error'; import { IUnleashStores } from '../../../lib/types'; import NameExistsError from '../../../lib/error/name-exists-error'; +import { EventService } from '../../../lib/services'; let stores: IUnleashStores; let db; let service: EnvironmentService; +let eventService: EventService; beforeAll(async () => { const config = createTestConfig(); db = await dbInit('environment_service_serial', config.getLogger); stores = db.stores; - service = new EnvironmentService(stores, config); + eventService = new EventService(stores, config); + service = new EnvironmentService(stores, config, eventService); }); afterAll(async () => { await db.destroy(); @@ -50,7 +53,7 @@ test('Can connect environment to project', async () => { description: '', stale: false, }); - await service.addEnvironmentToProject('test-connection', 'default'); + await service.addEnvironmentToProject('test-connection', 'default', 'user'); const overview = await stores.featureStrategiesStore.getFeatureOverview({ projectId: 'default', }); @@ -68,6 +71,13 @@ test('Can connect environment to project', async () => { }, ]); }); + const { events } = await eventService.getEvents(); + expect(events[0]).toMatchObject({ + type: 'project-environment-added', + project: 'default', + environment: 'test-connection', + createdBy: 'user', + }); }); test('Can remove environment from project', async () => { @@ -98,7 +108,11 @@ test('Can remove environment from project', async () => { }, ]); }); - await service.removeEnvironmentFromProject('removal-test', 'default'); + await service.removeEnvironmentFromProject( + 'removal-test', + 'default', + 'user', + ); overview = await stores.featureStrategiesStore.getFeatureOverview({ projectId: 'default', }); @@ -106,6 +120,13 @@ test('Can remove environment from project', async () => { overview.forEach((o) => { expect(o.environments).toEqual([]); }); + const { events } = await eventService.getEvents(); + expect(events[0]).toMatchObject({ + type: 'project-environment-removed', + project: 'default', + environment: 'removal-test', + createdBy: 'user', + }); }); test('Adding same environment twice should throw a NameExistsError', async () => { diff --git a/src/test/e2e/services/project-service.e2e.test.ts b/src/test/e2e/services/project-service.e2e.test.ts index feec8372c1..9c6b0c7ffe 100644 --- a/src/test/e2e/services/project-service.e2e.test.ts +++ b/src/test/e2e/services/project-service.e2e.test.ts @@ -64,7 +64,7 @@ beforeAll(async () => { featureToggleService = createFeatureToggleService(db.rawDatabase, config); - environmentService = new EnvironmentService(stores, config); + environmentService = new EnvironmentService(stores, config, eventService); projectService = createProjectService(db.rawDatabase, config); });