diff --git a/contract-tests/index.js b/contract-tests/index.js index d725439075..365917d193 100644 --- a/contract-tests/index.js +++ b/contract-tests/index.js @@ -32,6 +32,7 @@ app.get('/', (req, res) => { 'event-sampling', 'strongly-typed', 'inline-context', + 'anonymous-redaction', ], }); }); diff --git a/packages/shared/common/__tests__/internal/events/EventProcessor.test.ts b/packages/shared/common/__tests__/internal/events/EventProcessor.test.ts index d01fce4b96..80e9c7db62 100644 --- a/packages/shared/common/__tests__/internal/events/EventProcessor.test.ts +++ b/packages/shared/common/__tests__/internal/events/EventProcessor.test.ts @@ -1,5 +1,6 @@ import { clientContext, ContextDeduplicator } from '@launchdarkly/private-js-mocks'; +import { LDContextCommon, LDMultiKindContext } from '../../../dist'; import { Context } from '../../../src'; import { LDContextDeduplicator, LDDeliveryStatus, LDEventType } from '../../../src/api/subsystem'; import { EventProcessor, InputIdentifyEvent } from '../../../src/internal'; @@ -344,6 +345,92 @@ describe('given an event processor', () => { ]); }); + it('redacts all attributes from anonymous single-kind context for feature events', async () => { + const userObj = { key: 'user-key', kind: 'user', name: 'Example user', anonymous: true }; + const context = Context.fromLDContext(userObj); + + Date.now = jest.fn(() => 1000); + eventProcessor.sendEvent({ + kind: 'feature', + creationDate: 1000, + context, + key: 'flagkey', + version: 11, + variation: 1, + value: 'value', + trackEvents: true, + default: 'default', + samplingRatio: 1, + withReasons: true, + }); + + await eventProcessor.flush(); + + const redactedContext = { + kind: 'user', + key: 'user-key', + anonymous: true, + _meta: { + redactedAttributes: ['name'], + }, + }; + + const expectedIndexEvent = { ...testIndexEvent, context: userObj }; + const expectedFeatureEvent = { ...makeFeatureEvent(1000, 11, false), context: redactedContext }; + + expect(mockSendEventData).toBeCalledWith(LDEventType.AnalyticsEvents, [ + expectedIndexEvent, + expectedFeatureEvent, + makeSummary(1000, 1000, 1, 11), + ]); + }); + + it('redacts all attributes from anonymous multi-kind context for feature events', async () => { + const userObj: LDContextCommon = { key: 'user-key', name: 'Example user', anonymous: true }; + const org: LDContextCommon = { key: 'org-key', name: 'Example org' }; + const multi: LDMultiKindContext = { kind: 'multi', user: userObj, org }; + const context = Context.fromLDContext(multi); + + Date.now = jest.fn(() => 1000); + eventProcessor.sendEvent({ + kind: 'feature', + creationDate: 1000, + context, + key: 'flagkey', + version: 11, + variation: 1, + value: 'value', + trackEvents: true, + default: 'default', + samplingRatio: 1, + withReasons: true, + }); + + await eventProcessor.flush(); + + const redactedUserContext = { + key: 'user-key', + anonymous: true, + _meta: { + redactedAttributes: ['name'], + }, + }; + + const expectedIndexEvent = { ...testIndexEvent, context: multi }; + const expectedFeatureEvent = { + ...makeFeatureEvent(1000, 11, false), + context: { ...multi, user: redactedUserContext }, + }; + const expectedSummaryEvent = makeSummary(1000, 1000, 1, 11); + expectedSummaryEvent.features.flagkey.contextKinds = ['user', 'org']; + + expect(mockSendEventData).toBeCalledWith(LDEventType.AnalyticsEvents, [ + expectedIndexEvent, + expectedFeatureEvent, + expectedSummaryEvent, + ]); + }); + it('expires debug mode based on client time if client time is later than server time', async () => { Date.now = jest.fn(() => 2000); diff --git a/packages/shared/common/src/ContextFilter.ts b/packages/shared/common/src/ContextFilter.ts index 4cba50d478..f0373618d1 100644 --- a/packages/shared/common/src/ContextFilter.ts +++ b/packages/shared/common/src/ContextFilter.ts @@ -98,32 +98,49 @@ export default class ContextFilter { private readonly privateAttributes: AttributeReference[], ) {} - filter(context: Context): any { + filter(context: Context, redactAnonymousAttributes: boolean = false): any { const contexts = context.getContexts(); if (contexts.length === 1) { - return this.filterSingleKind(context, contexts[0][1], contexts[0][0]); + return this.filterSingleKind( + context, + contexts[0][1], + contexts[0][0], + redactAnonymousAttributes, + ); } const filteredMulti: any = { kind: 'multi', }; contexts.forEach(([kind, single]) => { - filteredMulti[kind] = this.filterSingleKind(context, single, kind); + filteredMulti[kind] = this.filterSingleKind(context, single, kind, redactAnonymousAttributes); }); return filteredMulti; } - private getAttributesToFilter(context: Context, single: LDContextCommon, kind: string) { + private getAttributesToFilter( + context: Context, + single: LDContextCommon, + kind: string, + redactAllAttributes: boolean, + ) { return ( - this.allAttributesPrivate + redactAllAttributes ? Object.keys(single).map((k) => new AttributeReference(k, true)) : [...this.privateAttributes, ...context.privateAttributes(kind)] ).filter((attr) => !protectedAttributes.some((protectedAttr) => protectedAttr.compare(attr))); } - private filterSingleKind(context: Context, single: LDContextCommon, kind: string): any { + private filterSingleKind( + context: Context, + single: LDContextCommon, + kind: string, + redactAnonymousAttributes: boolean, + ): any { + const redactAllAttributes = + this.allAttributesPrivate || (redactAnonymousAttributes && single.anonymous === true); const { cloned, excluded } = cloneWithRedactions( single, - this.getAttributesToFilter(context, single, kind), + this.getAttributesToFilter(context, single, kind, redactAllAttributes), ); if (context.legacy) { diff --git a/packages/shared/common/src/internal/events/EventProcessor.ts b/packages/shared/common/src/internal/events/EventProcessor.ts index 9abac2e051..dec984517b 100644 --- a/packages/shared/common/src/internal/events/EventProcessor.ts +++ b/packages/shared/common/src/internal/events/EventProcessor.ts @@ -261,7 +261,7 @@ export default class EventProcessor implements LDEventProcessor { const out: FeatureOutputEvent = { kind: debug ? 'debug' : 'feature', creationDate: event.creationDate, - context: this.contextFilter.filter(event.context), + context: this.contextFilter.filter(event.context, !debug), key: event.key, value: event.value, default: event.default,