Skip to content

Commit 4387375

Browse files
committed
feat: Redact anonymous attributes within feature events (#77)
1 parent b2ebcbf commit 4387375

File tree

5 files changed

+109
-5
lines changed

5 files changed

+109
-5
lines changed

apps/flutter_client_contract_test_service/bin/contract_test_service.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class TestApiImpl extends SdkTestApi {
2424
'client-independence',
2525
'context-comparison',
2626
'inline-context',
27+
'anonymous-redaction'
2728
];
2829

2930
static const clientUrlPrefix = '/client/';

packages/common/lib/src/serialization/event_serialization.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ final class EvalEventSerialization {
5151
json['context'] = LDContextSerialization.toJson(event.context,
5252
isEvent: true,
5353
allAttributesPrivate: allAttributesPrivate,
54-
globalPrivateAttributes: globalPrivateAttributes);
54+
globalPrivateAttributes: globalPrivateAttributes,
55+
redactAnonymous: !isDebug);
5556

5657
if (event.version != null) {
5758
json['version'] = event.version;

packages/common/lib/src/serialization/ld_context_serialization.dart

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,11 +165,16 @@ final class LDContextSerialization {
165165
/// references, then those attributes will be redacted in all context
166166
/// kinds.
167167
///
168+
/// if [isEvent] is true, and [redactAnonymous] is true, then for any
169+
/// anonymous context provided, all attributes will be redacted regardless of
170+
/// the [allAttributesPrivate] or [globalPrivateAttributes] settings.
171+
///
168172
/// Attempting to serialize an invalid context will return null.
169173
static Map<String, dynamic>? toJson(LDContext context,
170174
{required bool isEvent,
171175
bool allAttributesPrivate = false,
172-
Set<AttributeReference>? globalPrivateAttributes}) {
176+
Set<AttributeReference>? globalPrivateAttributes,
177+
bool redactAnonymous = false}) {
173178
if (!context.valid) {
174179
// Cannot serialize an invalid context.
175180
return null;
@@ -179,7 +184,8 @@ final class LDContextSerialization {
179184
Map<String, dynamic> result = {
180185
..._LDContextAttributesSerialization.toJson(attributes,
181186
isEvent: isEvent,
182-
allAttributesPrivate: allAttributesPrivate,
187+
allAttributesPrivate: allAttributesPrivate ||
188+
(redactAnonymous && attributes.anonymous),
183189
globalPrivateAttributes: globalPrivateAttributes)
184190
};
185191
result['kind'] = attributes.kind;
@@ -191,7 +197,8 @@ final class LDContextSerialization {
191197
result[attributes.kind] = _LDContextAttributesSerialization.toJson(
192198
attributes,
193199
isEvent: isEvent,
194-
allAttributesPrivate: allAttributesPrivate,
200+
allAttributesPrivate: allAttributesPrivate ||
201+
(redactAnonymous && attributes.anonymous),
195202
globalPrivateAttributes: globalPrivateAttributes);
196203
}
197204
return result;

packages/common/test/events/event_processor_test.dart

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,11 @@ void main() {
163163
final (processor, _) = createProcessor(innerClient);
164164

165165
final inputEvalEvent = EvalEvent(
166-
context: LDContextBuilder().kind('user', 'user-key').build(),
166+
context: LDContextBuilder()
167+
.kind('user', 'user-key')
168+
.setString('name', 'Example Name')
169+
.anonymous(true)
170+
.build(),
167171
flagKey: 'the-flag',
168172
defaultValue: LDValue.ofNum(10),
169173
evaluationDetail: LDEvaluationDetail(
@@ -186,6 +190,12 @@ void main() {
186190
expect(ldValueEvalEvent.getFor('kind').stringValue(), 'feature');
187191
expect(ldValueEvalEvent.getFor('creationDate').intValue(),
188192
inputEvalEvent.creationDate.millisecondsSinceEpoch);
193+
expect(
194+
ldValueEvalEvent
195+
.getFor('context')
196+
.getFor('_meta')
197+
.getFor('redactedAttributes'),
198+
LDValue.buildArray().addString('/name').build());
189199

190200
final ldValueSummaryEvent = decodedAsLdValue.get(1);
191201
// Not validating each field, as the serialization tests handle that.

packages/common/test/serialization/ld_context_serialization_test.dart

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,23 @@ void main() {
8585
.addString('dizzle', 'hgi')
8686
.build();
8787

88+
final expectedAnonymousContextWithFullRedaction = LDValueObjectBuilder()
89+
.addString('kind', 'organization')
90+
.addString('key', 'abc')
91+
.addBool('anonymous', true)
92+
.addValue(
93+
'_meta',
94+
LDValueObjectBuilder()
95+
.addValue(
96+
'redactedAttributes',
97+
LDValueArrayBuilder()
98+
.addString('/firstName')
99+
.addString('/bizzle')
100+
.addString('/dizzle')
101+
.build())
102+
.build())
103+
.build();
104+
88105
group('when it is serializing as a context', () {
89106
final isEvent = false;
90107
test('it includes all the attributes and the non-redacted meta data form',
@@ -147,6 +164,22 @@ void main() {
147164
expectedAnonymousContext);
148165
});
149166

167+
test('redactAnonymous only affects anonymous contexts', () {
168+
final encoded = LDContextSerialization.toJson(basicContext,
169+
isEvent: isEvent, redactAnonymous: true);
170+
expect(LDValueSerialization.fromJson(encoded), expectedBasicContext);
171+
172+
final encodedWithName = LDContextSerialization.toJson(contextWithName,
173+
isEvent: isEvent, redactAnonymous: true);
174+
expect(LDValueSerialization.fromJson(encodedWithName),
175+
expectedContextWithName);
176+
177+
final anonymousEncoded = LDContextSerialization.toJson(anonymousContext,
178+
isEvent: isEvent, redactAnonymous: true);
179+
expect(LDValueSerialization.fromJson(anonymousEncoded),
180+
expectedAnonymousContextWithFullRedaction);
181+
});
182+
150183
test(
151184
'it redacts all non-protected attributes when allAttributesPrivate = true',
152185
() {
@@ -277,6 +310,7 @@ void main() {
277310
group('given a multi-kind context', () {
278311
final orgAndUserContext = LDContextBuilder()
279312
.kind('organization', 'LD')
313+
.anonymous(true)
280314
.setBool('rocks', true)
281315
.name('name')
282316
.setValue('department',
@@ -317,6 +351,7 @@ void main() {
317351
'organization',
318352
LDValueObjectBuilder()
319353
.addString('key', 'LD')
354+
.addBool('anonymous', true)
320355
.addValue(
321356
'_meta',
322357
LDValueObjectBuilder()
@@ -363,6 +398,7 @@ void main() {
363398
LDValueObjectBuilder()
364399
.addString('key', 'LD')
365400
.addBool('rocks', true)
401+
.addBool('anonymous', true)
366402
.addString('name', 'name')
367403
.addValue(
368404
'department',
@@ -392,6 +428,54 @@ void main() {
392428
.build());
393429
});
394430

431+
test('it should only redact anonymous attributes from anonymous contexts',
432+
() {
433+
final encoded = LDContextSerialization.toJson(orgAndUserContext,
434+
isEvent: isEvent, redactAnonymous: true);
435+
436+
expect(
437+
LDValueSerialization.fromJson(encoded),
438+
LDValueObjectBuilder()
439+
.addString('kind', 'multi')
440+
.addValue(
441+
'organization',
442+
LDValueObjectBuilder()
443+
.addString('key', 'LD')
444+
.addBool('anonymous', true)
445+
.addValue(
446+
'_meta',
447+
LDValueObjectBuilder()
448+
.addValue(
449+
'redactedAttributes',
450+
LDValueArrayBuilder()
451+
.addString('/name')
452+
.addString('/rocks')
453+
.addString('/department')
454+
.build())
455+
.build())
456+
.build())
457+
.addValue(
458+
'user',
459+
LDValueObjectBuilder()
460+
.addString('key', 'abc')
461+
.addValue('object',
462+
LDValueObjectBuilder().addString('a', 'a').build())
463+
.addNum('order', 3.0)
464+
.addString('name', 'alphabet')
465+
.addValue(
466+
'_meta',
467+
LDValueObjectBuilder()
468+
.addValue(
469+
'redactedAttributes',
470+
LDValueArrayBuilder()
471+
.addString('letters')
472+
.addString('/object/b')
473+
.build())
474+
.build())
475+
.build())
476+
.build());
477+
});
478+
395479
test('it should apply global private attributes to all contexts', () {
396480
final encoded = LDContextSerialization.toJson(orgAndUserContext,
397481
isEvent: isEvent,
@@ -406,6 +490,7 @@ void main() {
406490
LDValueObjectBuilder()
407491
.addString('key', 'LD')
408492
.addBool('rocks', true)
493+
.addBool('anonymous', true)
409494
.addValue(
410495
'department',
411496
LDValueObjectBuilder()

0 commit comments

Comments
 (0)