Skip to content

Commit

Permalink
feat: Redact anonymous attributes within feature events (#352)
Browse files Browse the repository at this point in the history
  • Loading branch information
keelerm84 authored Feb 6, 2024
1 parent 381f008 commit 96b7e11
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 8 deletions.
1 change: 1 addition & 0 deletions contract-tests/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ app.get('/', (req, res) => {
'event-sampling',
'strongly-typed',
'inline-context',
'anonymous-redaction',
],
});
});
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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);

Expand Down
31 changes: 24 additions & 7 deletions packages/shared/common/src/ContextFilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit 96b7e11

Please sign in to comment.