From 3e7b9e64b84b09db4d4165a740d5c81a5c6f9115 Mon Sep 17 00:00:00 2001 From: Matthew Keeler Date: Mon, 20 Jul 2026 12:08:05 -0400 Subject: [PATCH 1/3] fix: Redact anonymous context attributes in migration op events Migration op events inline the full context but passed no redactAnonymous flag to ContextFilter.filter, so anonymous contexts kept their attributes in the migration_op event even though feature events redacted them. Pass true (migration events are never debug), matching feature-event behavior, and add an EventProcessor regression test. Fixes SDK-2722. --- .../internal/events/EventProcessor.test.ts | 48 +++++++++++++++++++ .../src/internal/events/EventProcessor.ts | 6 ++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/packages/shared/common/__tests__/internal/events/EventProcessor.test.ts b/packages/shared/common/__tests__/internal/events/EventProcessor.test.ts index aaf33ae0ee..614a03025f 100644 --- a/packages/shared/common/__tests__/internal/events/EventProcessor.test.ts +++ b/packages/shared/common/__tests__/internal/events/EventProcessor.test.ts @@ -468,6 +468,54 @@ describe('given an event processor', () => { ]); }); + it('redacts all attributes from anonymous context for migration op 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: 'migration_op', + operation: 'read', + creationDate: 1000, + context, + evaluation: { + key: 'flagkey', + value: 'live', + default: 'off', + reason: { kind: 'FALLTHROUGH' }, + }, + measurements: [], + samplingRatio: 1, + }); + + await eventProcessor.flush(); + + const redactedContext = { + kind: 'user', + key: 'user-key', + anonymous: true, + _meta: { + redactedAttributes: ['name'], + }, + }; + + expect(mockSendEventData).toBeCalledWith(LDEventType.AnalyticsEvents, [ + { + kind: 'migration_op', + operation: 'read', + creationDate: 1000, + context: redactedContext, + evaluation: { + key: 'flagkey', + value: 'live', + default: 'off', + reason: { kind: 'FALLTHROUGH' }, + }, + measurements: [], + }, + ]); + }); + 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/internal/events/EventProcessor.ts b/packages/shared/common/src/internal/events/EventProcessor.ts index 401d2fc4b6..616fcb8d05 100644 --- a/packages/shared/common/src/internal/events/EventProcessor.ts +++ b/packages/shared/common/src/internal/events/EventProcessor.ts @@ -263,7 +263,11 @@ export default class EventProcessor implements LDEventProcessor { if (shouldSample(inputEvent.samplingRatio)) { const migrationEvent: MigrationOutputEvent = { ...inputEvent, - context: inputEvent.context ? this._contextFilter.filter(inputEvent.context) : undefined, + // Migration events inline the full context and are never debug events, so anonymous + // context attributes must be redacted here just as they are for feature events. + context: inputEvent.context + ? this._contextFilter.filter(inputEvent.context, true) + : undefined, }; if (migrationEvent.samplingRatio === 1) { delete migrationEvent.samplingRatio; From 14704e7255ada3d14707080b88efdde7c8c6d47e Mon Sep 17 00:00:00 2001 From: Matthew Keeler Date: Mon, 20 Jul 2026 15:41:33 -0400 Subject: [PATCH 2/3] fix: Redact anonymous context attributes in custom events Like migration op events, custom events inline the full context but did not pass the redactAnonymous flag to ContextFilter.filter, so anonymous contexts kept their attributes in custom events. Pass true (custom events are never debug), matching feature and migration op behavior, and update the custom anonymous-context test to assert redaction. Fixes SDK-2722. --- .../__tests__/internal/events/EventProcessor.test.ts | 11 ++++++++++- .../common/src/internal/events/EventProcessor.ts | 4 +++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/shared/common/__tests__/internal/events/EventProcessor.test.ts b/packages/shared/common/__tests__/internal/events/EventProcessor.test.ts index 614a03025f..dd3285565c 100644 --- a/packages/shared/common/__tests__/internal/events/EventProcessor.test.ts +++ b/packages/shared/common/__tests__/internal/events/EventProcessor.test.ts @@ -737,6 +737,15 @@ describe('given an event processor', () => { await eventProcessor.flush(); + // The custom event inlines the context, so an anonymous context's attributes are redacted. + // The index event is not subject to anonymous redaction and keeps the full context. + const redactedAnonUser = { + key: 'anon-user', + kind: 'user', + anonymous: true, + _meta: { redactedAttributes: ['name'] }, + }; + expect(mockSendEventData).toBeCalledWith(LDEventType.AnalyticsEvents, [ { kind: 'index', @@ -748,7 +757,7 @@ describe('given an event processor', () => { key: 'eventkey', data: { thing: 'stuff' }, creationDate: 1000, - context: { ...anonUser, kind: 'user' }, + context: redactedAnonUser, }, ]); }); diff --git a/packages/shared/common/src/internal/events/EventProcessor.ts b/packages/shared/common/src/internal/events/EventProcessor.ts index 616fcb8d05..e0cb069b47 100644 --- a/packages/shared/common/src/internal/events/EventProcessor.ts +++ b/packages/shared/common/src/internal/events/EventProcessor.ts @@ -363,7 +363,9 @@ export default class EventProcessor implements LDEventProcessor { kind: 'custom', creationDate: event.creationDate, key: event.key, - context: this._contextFilter.filter(event.context), + // Custom events inline the full context and are never debug events, so anonymous + // context attributes must be redacted here just as they are for feature events. + context: this._contextFilter.filter(event.context, true), }; if (event.samplingRatio !== 1) { From 7a4f371a377a2bc6fbae348fdf0053634bb635a7 Mon Sep 17 00:00:00 2001 From: Matthew Keeler Date: Mon, 20 Jul 2026 15:47:27 -0400 Subject: [PATCH 3/3] remove unncessary comments --- packages/shared/common/src/internal/events/EventProcessor.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/shared/common/src/internal/events/EventProcessor.ts b/packages/shared/common/src/internal/events/EventProcessor.ts index e0cb069b47..354533cc09 100644 --- a/packages/shared/common/src/internal/events/EventProcessor.ts +++ b/packages/shared/common/src/internal/events/EventProcessor.ts @@ -263,8 +263,6 @@ export default class EventProcessor implements LDEventProcessor { if (shouldSample(inputEvent.samplingRatio)) { const migrationEvent: MigrationOutputEvent = { ...inputEvent, - // Migration events inline the full context and are never debug events, so anonymous - // context attributes must be redacted here just as they are for feature events. context: inputEvent.context ? this._contextFilter.filter(inputEvent.context, true) : undefined, @@ -363,8 +361,6 @@ export default class EventProcessor implements LDEventProcessor { kind: 'custom', creationDate: event.creationDate, key: event.key, - // Custom events inline the full context and are never debug events, so anonymous - // context attributes must be redacted here just as they are for feature events. context: this._contextFilter.filter(event.context, true), };