diff --git a/src/lib/experimental.ts b/src/lib/experimental.ts index 5a4934c023a..9c258f415d6 100644 --- a/src/lib/experimental.ts +++ b/src/lib/experimental.ts @@ -2,6 +2,7 @@ export interface IExperimentalOptions { metricsV2?: IExperimentalToggle; clientFeatureMemoize?: IExperimentalToggle; segments?: IExperimentalSegments; + anonymiseEventLog?: boolean; } export interface IExperimentalToggle { diff --git a/src/lib/routes/admin-api/event.ts b/src/lib/routes/admin-api/event.ts index c03a219cf11..ac9913fae63 100644 --- a/src/lib/routes/admin-api/event.ts +++ b/src/lib/routes/admin-api/event.ts @@ -5,22 +5,35 @@ import EventService from '../../services/event-service'; import { ADMIN } from '../../types/permissions'; import { IEvent } from '../../types/events'; import Controller from '../controller'; +import { anonymise } from '../../util/anonymise'; const version = 1; - export default class EventController extends Controller { private eventService: EventService; + private anonymise: boolean = false; + constructor( config: IUnleashConfig, { eventService }: Pick, ) { super(config); this.eventService = eventService; + this.anonymise = config.experimental?.anonymiseEventLog; this.get('/', this.getEvents, ADMIN); this.get('/:name', this.getEventsForToggle); } + fixEvents(events: IEvent[]): IEvent[] { + if (this.anonymise) { + return events.map((e: IEvent) => ({ + ...e, + createdBy: anonymise(e.createdBy), + })); + } + return events; + } + async getEvents( req: Request, res: Response, @@ -32,7 +45,10 @@ export default class EventController extends Controller { } else { events = await this.eventService.getEvents(); } - res.json({ version, events }); + res.json({ + version, + events: this.fixEvents(events), + }); } async getEventsForToggle( @@ -42,10 +58,10 @@ export default class EventController extends Controller { const toggleName = req.params.name; const events = await this.eventService.getEventsForToggle(toggleName); - if (events) { - res.json({ toggleName, events }); - } else { - res.status(404).json({ error: 'Could not find events' }); - } + res.json({ + version, + toggleName, + events: this.fixEvents(events), + }); } } diff --git a/src/lib/routes/admin-api/events.test.ts b/src/lib/routes/admin-api/events.test.ts index 48291643720..333300229bf 100644 --- a/src/lib/routes/admin-api/events.test.ts +++ b/src/lib/routes/admin-api/events.test.ts @@ -5,12 +5,14 @@ import { createTestConfig } from '../../../test/config/test-config'; import createStores from '../../../test/fixtures/store'; import getApp from '../../app'; +import { FeatureCreatedEvent } from '../../types/events'; -async function getSetup() { +async function getSetup(anonymise: boolean = false) { const base = `/random${Math.round(Math.random() * 1000)}`; const stores = createStores(); const config = createTestConfig({ server: { baseUriPath: base }, + experimental: { anonymiseEventLog: anonymise }, }); const services = createServices(stores, config); const app = await getApp(config, stores, services); @@ -29,3 +31,43 @@ test('should get empty events list via admin', async () => { expect(res.body.events.length === 0).toBe(true); }); }); + +test('should get events list via admin', async () => { + const { request, base, eventStore } = await getSetup(); + eventStore.store( + new FeatureCreatedEvent({ + createdBy: 'some@email.com', + data: { name: 'test', project: 'default' }, + featureName: 'test', + project: 'default', + tags: [], + }), + ); + const { body } = await request + .get(`${base}/api/admin/events`) + .expect('Content-Type', /json/) + .expect(200); + + expect(body.events.length).toBe(1); + expect(body.events[0].createdBy).toBe('some@email.com'); +}); + +test('should anonymise events list via admin', async () => { + const { request, base, eventStore } = await getSetup(true); + eventStore.store( + new FeatureCreatedEvent({ + createdBy: 'some@email.com', + data: { name: 'test', project: 'default' }, + featureName: 'test', + project: 'default', + tags: [], + }), + ); + const { body } = await request + .get(`${base}/api/admin/events`) + .expect('Content-Type', /json/) + .expect(200); + + expect(body.events.length).toBe(1); + expect(body.events[0].createdBy).toBe('676212ff7@unleash.run'); +}); diff --git a/src/lib/util/anonymise.ts b/src/lib/util/anonymise.ts new file mode 100644 index 00000000000..a502c86be35 --- /dev/null +++ b/src/lib/util/anonymise.ts @@ -0,0 +1,9 @@ +import { createHash } from 'crypto'; + +export function anonymise(s: string): string { + const hash = createHash('sha256') + .update(s, 'utf-8') + .digest('hex') + .slice(0, 9); + return `${hash}@unleash.run`; +} diff --git a/src/server-dev.ts b/src/server-dev.ts index d790a9e763a..b15ae7d4d10 100644 --- a/src/server-dev.ts +++ b/src/server-dev.ts @@ -34,6 +34,7 @@ process.nextTick(async () => { experimental: { metricsV2: { enabled: true }, segments: experimentalSegmentsConfig(), + anonymiseEventLog: false, }, authentication: { initApiTokens: [