diff --git a/packages/sdk/electron/contract-tests/entity/src/main.ts b/packages/sdk/electron/contract-tests/entity/src/main.ts index 50ab3f72b1..6a9944a0b7 100644 --- a/packages/sdk/electron/contract-tests/entity/src/main.ts +++ b/packages/sdk/electron/contract-tests/entity/src/main.ts @@ -24,7 +24,6 @@ const capabilities = [ 'client-per-context-summaries', 'track-hooks', 'tls:skip-verify-peer', - 'secure-mode-hash', 'event-gzip', /// //////////////////////////// diff --git a/packages/shared/common/__tests__/internal/events/EventProcessor.test.ts b/packages/shared/common/__tests__/internal/events/EventProcessor.test.ts index dd3285565c..f0e6579f3b 100644 --- a/packages/shared/common/__tests__/internal/events/EventProcessor.test.ts +++ b/packages/shared/common/__tests__/internal/events/EventProcessor.test.ts @@ -472,8 +472,17 @@ describe('given an event processor', () => { const userObj = { key: 'user-key', kind: 'user', name: 'Example user', anonymous: true }; const context = Context.fromLDContext(userObj); + // Migration events are server-only; servers set redactAnonymousAllEvents: true so anonymous + // contexts are redacted. + const serverEventProcessor = new EventProcessor( + { ...eventProcessorConfig, redactAnonymousAllEvents: true }, + clientContext, + {}, + new ContextDeduplicator(), + ); + Date.now = jest.fn(() => 1000); - eventProcessor.sendEvent({ + serverEventProcessor.sendEvent({ kind: 'migration_op', operation: 'read', creationDate: 1000, @@ -488,7 +497,8 @@ describe('given an event processor', () => { samplingRatio: 1, }); - await eventProcessor.flush(); + await serverEventProcessor.flush(); + serverEventProcessor.close(); const redactedContext = { kind: 'user', @@ -737,8 +747,46 @@ 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. + // By default (redactAnonymousAllEvents unset -> false, the client-side behavior) custom events + // do NOT redact an anonymous context's attributes; the full context is sent. + expect(mockSendEventData).toBeCalledWith(LDEventType.AnalyticsEvents, [ + { + kind: 'index', + creationDate: 1000, + context: { ...anonUser, kind: 'user' }, + }, + { + kind: 'custom', + key: 'eventkey', + data: { thing: 'stuff' }, + creationDate: 1000, + context: { ...anonUser, kind: 'user' }, + }, + ]); + }); + + it('redacts anonymous context in custom event when redactAnonymousAllEvents is true', async () => { + // Server-side SDKs set redactAnonymousAllEvents: true so custom events redact an anonymous + // context's attributes, just like feature events. + const serverEventProcessor = new EventProcessor( + { ...eventProcessorConfig, redactAnonymousAllEvents: true }, + clientContext, + {}, + new ContextDeduplicator(), + ); + + serverEventProcessor.sendEvent({ + kind: 'custom', + creationDate: 1000, + context: Context.fromLDContext(anonUser), + key: 'eventkey', + data: { thing: 'stuff' }, + samplingRatio: 1, + }); + + await serverEventProcessor.flush(); + serverEventProcessor.close(); + const redactedAnonUser = { key: 'anon-user', kind: 'user', diff --git a/packages/shared/common/src/internal/events/EventProcessor.ts b/packages/shared/common/src/internal/events/EventProcessor.ts index 354533cc09..18b4dd07d4 100644 --- a/packages/shared/common/src/internal/events/EventProcessor.ts +++ b/packages/shared/common/src/internal/events/EventProcessor.ts @@ -109,6 +109,11 @@ export interface EventProcessorOptions { eventsCapacity: number; flushInterval: number; diagnosticRecordingInterval: number; + // Whether anonymous context attributes are redacted across all inlined events (custom and + // migration op events), in addition to feature events which always redact them. Defaults to + // false (redact only feature events, the client-side behavior); server-side SDKs set this to + // true so anonymous contexts are redacted on every event kind. + redactAnonymousAllEvents?: boolean; } function isMultiEventSummarizer(summarizer: unknown): summarizer is LDMultiEventSummarizer { @@ -264,7 +269,10 @@ export default class EventProcessor implements LDEventProcessor { const migrationEvent: MigrationOutputEvent = { ...inputEvent, context: inputEvent.context - ? this._contextFilter.filter(inputEvent.context, true) + ? this._contextFilter.filter( + inputEvent.context, + this._config.redactAnonymousAllEvents ?? false, + ) : undefined, }; if (migrationEvent.samplingRatio === 1) { @@ -361,7 +369,10 @@ export default class EventProcessor implements LDEventProcessor { kind: 'custom', creationDate: event.creationDate, key: event.key, - context: this._contextFilter.filter(event.context, true), + context: this._contextFilter.filter( + event.context, + this._config.redactAnonymousAllEvents ?? false, + ), }; if (event.samplingRatio !== 1) { diff --git a/packages/shared/sdk-server/src/LDClientImpl.ts b/packages/shared/sdk-server/src/LDClientImpl.ts index 6af44c9137..8836aaa580 100644 --- a/packages/shared/sdk-server/src/LDClientImpl.ts +++ b/packages/shared/sdk-server/src/LDClientImpl.ts @@ -166,7 +166,9 @@ function constructFDv1( eventProcessor = new NullEventProcessor(); } else { eventProcessor = new internal.EventProcessor( - config, + // Server-side SDKs redact anonymous context attributes on all inlined events (feature, + // custom, migration op), not just feature events. + { ...config, redactAnonymousAllEvents: true }, clientContext, baseHeaders, new ContextDeduplicator(config), @@ -312,7 +314,9 @@ function constructFDv2( eventProcessor = new NullEventProcessor(); } else { eventProcessor = new internal.EventProcessor( - config, + // Server-side SDKs redact anonymous context attributes on all inlined events (feature, + // custom, migration op), not just feature events. + { ...config, redactAnonymousAllEvents: true }, clientContext, baseHeaders, new ContextDeduplicator(config),