Skip to content

Commit 8f7ad7e

Browse files
committed
feat: Redact anonymous attributes within feature events (#352)
1 parent 961d21b commit 8f7ad7e

File tree

4 files changed

+113
-8
lines changed

4 files changed

+113
-8
lines changed

contract-tests/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ app.get('/', (req, res) => {
3333
'strongly-typed',
3434
'polling-gzip',
3535
'inline-context',
36+
'anonymous-redaction',
3637
],
3738
});
3839
});

packages/shared/common/__tests__/internal/events/EventProcessor.test.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { clientContext, ContextDeduplicator } from '@launchdarkly/private-js-mocks';
22

3+
import { LDContextCommon, LDMultiKindContext } from '../../../dist';
34
import { Context } from '../../../src';
45
import { LDContextDeduplicator, LDDeliveryStatus, LDEventType } from '../../../src/api/subsystem';
56
import { EventProcessor, InputIdentifyEvent } from '../../../src/internal';
@@ -344,6 +345,92 @@ describe('given an event processor', () => {
344345
]);
345346
});
346347

348+
it('redacts all attributes from anonymous single-kind context for feature events', async () => {
349+
const userObj = { key: 'user-key', kind: 'user', name: 'Example user', anonymous: true };
350+
const context = Context.fromLDContext(userObj);
351+
352+
Date.now = jest.fn(() => 1000);
353+
eventProcessor.sendEvent({
354+
kind: 'feature',
355+
creationDate: 1000,
356+
context,
357+
key: 'flagkey',
358+
version: 11,
359+
variation: 1,
360+
value: 'value',
361+
trackEvents: true,
362+
default: 'default',
363+
samplingRatio: 1,
364+
withReasons: true,
365+
});
366+
367+
await eventProcessor.flush();
368+
369+
const redactedContext = {
370+
kind: 'user',
371+
key: 'user-key',
372+
anonymous: true,
373+
_meta: {
374+
redactedAttributes: ['name'],
375+
},
376+
};
377+
378+
const expectedIndexEvent = { ...testIndexEvent, context: userObj };
379+
const expectedFeatureEvent = { ...makeFeatureEvent(1000, 11, false), context: redactedContext };
380+
381+
expect(mockSendEventData).toBeCalledWith(LDEventType.AnalyticsEvents, [
382+
expectedIndexEvent,
383+
expectedFeatureEvent,
384+
makeSummary(1000, 1000, 1, 11),
385+
]);
386+
});
387+
388+
it('redacts all attributes from anonymous multi-kind context for feature events', async () => {
389+
const userObj: LDContextCommon = { key: 'user-key', name: 'Example user', anonymous: true };
390+
const org: LDContextCommon = { key: 'org-key', name: 'Example org' };
391+
const multi: LDMultiKindContext = { kind: 'multi', user: userObj, org };
392+
const context = Context.fromLDContext(multi);
393+
394+
Date.now = jest.fn(() => 1000);
395+
eventProcessor.sendEvent({
396+
kind: 'feature',
397+
creationDate: 1000,
398+
context,
399+
key: 'flagkey',
400+
version: 11,
401+
variation: 1,
402+
value: 'value',
403+
trackEvents: true,
404+
default: 'default',
405+
samplingRatio: 1,
406+
withReasons: true,
407+
});
408+
409+
await eventProcessor.flush();
410+
411+
const redactedUserContext = {
412+
key: 'user-key',
413+
anonymous: true,
414+
_meta: {
415+
redactedAttributes: ['name'],
416+
},
417+
};
418+
419+
const expectedIndexEvent = { ...testIndexEvent, context: multi };
420+
const expectedFeatureEvent = {
421+
...makeFeatureEvent(1000, 11, false),
422+
context: { ...multi, user: redactedUserContext },
423+
};
424+
const expectedSummaryEvent = makeSummary(1000, 1000, 1, 11);
425+
expectedSummaryEvent.features.flagkey.contextKinds = ['user', 'org'];
426+
427+
expect(mockSendEventData).toBeCalledWith(LDEventType.AnalyticsEvents, [
428+
expectedIndexEvent,
429+
expectedFeatureEvent,
430+
expectedSummaryEvent,
431+
]);
432+
});
433+
347434
it('expires debug mode based on client time if client time is later than server time', async () => {
348435
Date.now = jest.fn(() => 2000);
349436

packages/shared/common/src/ContextFilter.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -98,32 +98,49 @@ export default class ContextFilter {
9898
private readonly privateAttributes: AttributeReference[],
9999
) {}
100100

101-
filter(context: Context): any {
101+
filter(context: Context, redactAnonymousAttributes: boolean = false): any {
102102
const contexts = context.getContexts();
103103
if (contexts.length === 1) {
104-
return this.filterSingleKind(context, contexts[0][1], contexts[0][0]);
104+
return this.filterSingleKind(
105+
context,
106+
contexts[0][1],
107+
contexts[0][0],
108+
redactAnonymousAttributes,
109+
);
105110
}
106111
const filteredMulti: any = {
107112
kind: 'multi',
108113
};
109114
contexts.forEach(([kind, single]) => {
110-
filteredMulti[kind] = this.filterSingleKind(context, single, kind);
115+
filteredMulti[kind] = this.filterSingleKind(context, single, kind, redactAnonymousAttributes);
111116
});
112117
return filteredMulti;
113118
}
114119

115-
private getAttributesToFilter(context: Context, single: LDContextCommon, kind: string) {
120+
private getAttributesToFilter(
121+
context: Context,
122+
single: LDContextCommon,
123+
kind: string,
124+
redactAllAttributes: boolean,
125+
) {
116126
return (
117-
this.allAttributesPrivate
127+
redactAllAttributes
118128
? Object.keys(single).map((k) => new AttributeReference(k, true))
119129
: [...this.privateAttributes, ...context.privateAttributes(kind)]
120130
).filter((attr) => !protectedAttributes.some((protectedAttr) => protectedAttr.compare(attr)));
121131
}
122132

123-
private filterSingleKind(context: Context, single: LDContextCommon, kind: string): any {
133+
private filterSingleKind(
134+
context: Context,
135+
single: LDContextCommon,
136+
kind: string,
137+
redactAnonymousAttributes: boolean,
138+
): any {
139+
const redactAllAttributes =
140+
this.allAttributesPrivate || (redactAnonymousAttributes && single.anonymous === true);
124141
const { cloned, excluded } = cloneWithRedactions(
125142
single,
126-
this.getAttributesToFilter(context, single, kind),
143+
this.getAttributesToFilter(context, single, kind, redactAllAttributes),
127144
);
128145

129146
if (context.legacy) {

packages/shared/common/src/internal/events/EventProcessor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ export default class EventProcessor implements LDEventProcessor {
270270
const out: FeatureOutputEvent = {
271271
kind: debug ? 'debug' : 'feature',
272272
creationDate: event.creationDate,
273-
context: this.contextFilter.filter(event.context),
273+
context: this.contextFilter.filter(event.context, !debug),
274274
key: event.key,
275275
value: event.value,
276276
default: event.default,

0 commit comments

Comments
 (0)