From 3fba6d7f947bfa55a909e0d3daff3eb99ed3a63c Mon Sep 17 00:00:00 2001 From: Felipe Lima Date: Wed, 12 Feb 2025 15:26:56 -0800 Subject: [PATCH 01/10] feat(events): [WIP] New API `EppoAssignmentLogger --- src/client/eppo-client.ts | 17 +++++++++++++---- src/eppo-assignment-logger.ts | 20 ++++++++++++++++++++ 2 files changed, 33 insertions(+), 4 deletions(-) create mode 100644 src/eppo-assignment-logger.ts diff --git a/src/client/eppo-client.ts b/src/client/eppo-client.ts index 20e8861..c29001b 100644 --- a/src/client/eppo-client.ts +++ b/src/client/eppo-client.ts @@ -1171,10 +1171,19 @@ export default class EppoClient { } private maybeLogAssignment(result: FlagEvaluation) { - const { flagKey, format, subjectKey, allocationKey, subjectAttributes, variation } = result; + const { + flagKey, + format, + subjectKey, + allocationKey = null, + subjectAttributes, + variation, + flagEvaluationDetails, + extraLogging = {}, + } = result; const event: IAssignmentEvent = { - ...(result.extraLogging ?? {}), - allocation: allocationKey ?? null, + ...extraLogging, + allocation: allocationKey, experiment: allocationKey ? `${flagKey}-${allocationKey}` : null, featureFlag: flagKey, format, @@ -1183,7 +1192,7 @@ export default class EppoClient { timestamp: new Date().toISOString(), subjectAttributes, metaData: this.buildLoggerMetadata(), - evaluationDetails: result.flagEvaluationDetails, + evaluationDetails: flagEvaluationDetails, }; if (variation && allocationKey) { diff --git a/src/eppo-assignment-logger.ts b/src/eppo-assignment-logger.ts new file mode 100644 index 0000000..74167f1 --- /dev/null +++ b/src/eppo-assignment-logger.ts @@ -0,0 +1,20 @@ +import { IAssignmentEvent, IAssignmentLogger } from './assignment-logger'; +import EppoClient from './client/eppo-client'; + +/** TODO docs */ +export class EppoAssignmentLogger implements IAssignmentLogger { + constructor(private readonly eppoClient: EppoClient) {} + + logAssignment(event: IAssignmentEvent): void { + const entity = event.subjectAttributes.entity; + const { holdoutKey, holdoutVariation, subject: subject_id, experiment, variant } = event; + this.eppoClient.track('__eppo_assignment', { + subject_id, + experiment, + variant, + entity, + holdout: holdoutKey, + holdout_variant: holdoutVariation, + }); + } +} From 01ee04f53d588c1d0cf8d6a1ffbb51311484b24d Mon Sep 17 00:00:00 2001 From: Felipe Lima Date: Wed, 12 Feb 2025 15:29:23 -0800 Subject: [PATCH 02/10] minor cleanup --- src/eppo-assignment-logger.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/eppo-assignment-logger.ts b/src/eppo-assignment-logger.ts index 74167f1..544b184 100644 --- a/src/eppo-assignment-logger.ts +++ b/src/eppo-assignment-logger.ts @@ -7,14 +7,20 @@ export class EppoAssignmentLogger implements IAssignmentLogger { logAssignment(event: IAssignmentEvent): void { const entity = event.subjectAttributes.entity; - const { holdoutKey, holdoutVariation, subject: subject_id, experiment, variant } = event; + const { + holdoutKey: holdout, + holdoutVariation: holdout_variant, + subject: subject_id, + experiment, + variant, + } = event; this.eppoClient.track('__eppo_assignment', { subject_id, experiment, variant, entity, - holdout: holdoutKey, - holdout_variant: holdoutVariation, + holdout, + holdout_variant, }); } } From 111bb08cf24d5a0883fec53c4be11b0a08f1ae89 Mon Sep 17 00:00:00 2001 From: Felipe Lima Date: Wed, 5 Mar 2025 14:17:25 -0800 Subject: [PATCH 03/10] add entityId --- src/assignment-logger.ts | 35 ++++-------- src/client/eppo-client.ts | 4 +- src/eppo-assignment-logger.spec.ts | 88 ++++++++++++++++++++++++++++++ src/eppo-assignment-logger.ts | 22 +++++--- src/interfaces.ts | 1 + 5 files changed, 116 insertions(+), 34 deletions(-) create mode 100644 src/eppo-assignment-logger.spec.ts diff --git a/src/assignment-logger.ts b/src/assignment-logger.ts index 662fe6c..82079b6 100644 --- a/src/assignment-logger.ts +++ b/src/assignment-logger.ts @@ -13,34 +13,22 @@ export type NullableHoldoutVariationType = HoldoutVariationEnum | null; */ export interface IAssignmentEvent { - /** - * An Eppo allocation key - */ + /** * An Eppo allocation key */ allocation: string | null; - /** - * An Eppo experiment key - */ + /** * An Eppo experiment key */ experiment: string | null; - /** - * An Eppo feature flag key - */ + /** * An Eppo feature flag key */ featureFlag: string; - /** - * The assigned variation - */ + /** * The assigned variation */ variation: string | null; - /** - * The entity or user that was assigned to a variation - */ + /** * The entity or user that was assigned to a variation */ subject: string; - /** - * The time the subject was exposed to the variation. - */ + /** * The time the subject was exposed to the variation. */ timestamp: string; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -49,15 +37,14 @@ export interface IAssignmentEvent { metaData?: Record; - /** - * The format of the flag. - */ + /** * The format of the flag. */ format: string; - /** - * The flag evaluation details. Null if the flag was precomputed. - */ + /** * The flag evaluation details. Null if the flag was precomputed. */ evaluationDetails: IFlagEvaluationDetails | null; + + /** * The entity ID of the subject, if present. */ + entityId?: number; } /** diff --git a/src/client/eppo-client.ts b/src/client/eppo-client.ts index 0764b9f..7f19649 100644 --- a/src/client/eppo-client.ts +++ b/src/client/eppo-client.ts @@ -1288,7 +1288,7 @@ export default class EppoClient { }); } - private maybeLogAssignment(result: FlagEvaluation) { + private maybeLogAssignment(result: FlagEvaluation & { entityId?: number }) { const { flagKey, format, @@ -1298,6 +1298,7 @@ export default class EppoClient { variation, flagEvaluationDetails, extraLogging = {}, + entityId, } = result; const event: IAssignmentEvent = { ...extraLogging, @@ -1311,6 +1312,7 @@ export default class EppoClient { subjectAttributes, metaData: this.buildLoggerMetadata(), evaluationDetails: flagEvaluationDetails, + entityId, }; if (variation && allocationKey) { diff --git a/src/eppo-assignment-logger.spec.ts b/src/eppo-assignment-logger.spec.ts new file mode 100644 index 0000000..ccc6990 --- /dev/null +++ b/src/eppo-assignment-logger.spec.ts @@ -0,0 +1,88 @@ +import { IAssignmentEvent } from './assignment-logger'; +import EppoClient from './client/eppo-client'; +import { IConfigurationStore } from './configuration-store/configuration-store'; +import { EppoAssignmentLogger } from './eppo-assignment-logger'; +import { Flag } from './interfaces'; + +jest.mock('./client/eppo-client'); + +describe('EppoAssignmentLogger', () => { + let mockEppoClient: jest.Mocked; + let logger: EppoAssignmentLogger; + + beforeEach(() => { + jest.clearAllMocks(); + mockEppoClient = new EppoClient({ + flagConfigurationStore: {} as IConfigurationStore, + }) as jest.Mocked; + mockEppoClient.track = jest.fn(); + logger = new EppoAssignmentLogger(mockEppoClient); + }); + + it('should log assignment events correctly', () => { + // Arrange + const assignmentEvent: IAssignmentEvent = { + subject: 'user-123', + experiment: 'experiment-abc', + variant: 'control', + entityId: 456, // Changed to number + holdoutKey: 'holdout-xyz', + holdoutVariation: 'holdout-variant-1', + // Add required properties based on the IAssignmentEvent interface + allocation: 'allocation-1', + featureFlag: 'feature-flag-1', + variation: 'variation-1', + timestamp: new Date().toISOString(), + subjectAttributes: {}, + flagKey: 'flag-key-1', + format: 'json', + evaluationDetails: null, + }; + + // Act + logger.logAssignment(assignmentEvent); + + // Assert + expect(mockEppoClient.track).toHaveBeenCalledTimes(1); + expect(mockEppoClient.track).toHaveBeenCalledWith('__eppo_assignment', { + subject_id: 'user-123', + experiment: 'experiment-abc', + variant: 'control', + entity_id: 456, + holdout: 'holdout-xyz', + holdout_variant: 'holdout-variant-1', + }); + }); + + it('should handle missing optional fields', () => { + // Arrange + const assignmentEvent: IAssignmentEvent = { + subject: 'user-123', + experiment: 'experiment-abc', + variant: 'control', + // Add required properties based on the IAssignmentEvent interface + allocation: 'allocation-1', + featureFlag: 'feature-flag-1', + variation: 'variation-1', + timestamp: new Date().toISOString(), + subjectAttributes: {}, + flagKey: 'flag-key-1', + format: 'json', + evaluationDetails: null, + }; + + // Act + logger.logAssignment(assignmentEvent); + + // Assert + expect(mockEppoClient.track).toHaveBeenCalledTimes(1); + expect(mockEppoClient.track).toHaveBeenCalledWith('__eppo_assignment', { + subject_id: 'user-123', + experiment: 'experiment-abc', + variant: 'control', + entity_id: undefined, + holdout: undefined, + holdout_variant: undefined, + }); + }); +}); diff --git a/src/eppo-assignment-logger.ts b/src/eppo-assignment-logger.ts index 544b184..c47e999 100644 --- a/src/eppo-assignment-logger.ts +++ b/src/eppo-assignment-logger.ts @@ -1,26 +1,30 @@ import { IAssignmentEvent, IAssignmentLogger } from './assignment-logger'; import EppoClient from './client/eppo-client'; -/** TODO docs */ +/** + * Tracks an assignment event by submitting it to the Eppo Ingestion API. + * Events are queued up for delivery according to the EppoClient's `EventDispatcher` implementation. + */ export class EppoAssignmentLogger implements IAssignmentLogger { constructor(private readonly eppoClient: EppoClient) {} logAssignment(event: IAssignmentEvent): void { - const entity = event.subjectAttributes.entity; const { + entityId: entity_id, + experiment, holdoutKey: holdout, holdoutVariation: holdout_variant, subject: subject_id, - experiment, variant, } = event; - this.eppoClient.track('__eppo_assignment', { - subject_id, + const payload = { + entity_id, experiment, - variant, - entity, - holdout, holdout_variant, - }); + holdout, + subject_id, + variant, + }; + this.eppoClient.track('__eppo_assignment', payload); } } diff --git a/src/interfaces.ts b/src/interfaces.ts index c4cbe94..a84a848 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -59,6 +59,7 @@ export interface Flag { variations: Record; allocations: Allocation[]; totalShards: number; + entityId?: number; } export interface ObfuscatedFlag { From 6ae8a6fe6de8251fd0875e338486a7b37b65b43f Mon Sep 17 00:00:00 2001 From: Felipe Lima Date: Wed, 5 Mar 2025 14:23:48 -0800 Subject: [PATCH 04/10] skip tracking if no entityId --- src/assignment-logger.ts | 2 +- src/eppo-assignment-logger.spec.ts | 54 ++++++++++++++++++++++++++---- src/eppo-assignment-logger.ts | 7 ++++ 3 files changed, 56 insertions(+), 7 deletions(-) diff --git a/src/assignment-logger.ts b/src/assignment-logger.ts index 82079b6..0af6294 100644 --- a/src/assignment-logger.ts +++ b/src/assignment-logger.ts @@ -44,7 +44,7 @@ export interface IAssignmentEvent { evaluationDetails: IFlagEvaluationDetails | null; /** * The entity ID of the subject, if present. */ - entityId?: number; + entityId?: number | null; } /** diff --git a/src/eppo-assignment-logger.spec.ts b/src/eppo-assignment-logger.spec.ts index ccc6990..b1cb84a 100644 --- a/src/eppo-assignment-logger.spec.ts +++ b/src/eppo-assignment-logger.spec.ts @@ -25,16 +25,14 @@ describe('EppoAssignmentLogger', () => { subject: 'user-123', experiment: 'experiment-abc', variant: 'control', - entityId: 456, // Changed to number + entityId: 456, holdoutKey: 'holdout-xyz', holdoutVariation: 'holdout-variant-1', - // Add required properties based on the IAssignmentEvent interface allocation: 'allocation-1', featureFlag: 'feature-flag-1', variation: 'variation-1', timestamp: new Date().toISOString(), subjectAttributes: {}, - flagKey: 'flag-key-1', format: 'json', evaluationDetails: null, }; @@ -60,13 +58,12 @@ describe('EppoAssignmentLogger', () => { subject: 'user-123', experiment: 'experiment-abc', variant: 'control', - // Add required properties based on the IAssignmentEvent interface + entityId: 789, allocation: 'allocation-1', featureFlag: 'feature-flag-1', variation: 'variation-1', timestamp: new Date().toISOString(), subjectAttributes: {}, - flagKey: 'flag-key-1', format: 'json', evaluationDetails: null, }; @@ -80,9 +77,54 @@ describe('EppoAssignmentLogger', () => { subject_id: 'user-123', experiment: 'experiment-abc', variant: 'control', - entity_id: undefined, + entity_id: 789, holdout: undefined, holdout_variant: undefined, }); }); + + it('should skip tracking when entityId is null', () => { + // Arrange + const assignmentEvent: IAssignmentEvent = { + subject: 'user-123', + experiment: 'experiment-abc', + variant: 'control', + entityId: null, + allocation: 'allocation-1', + featureFlag: 'feature-flag-1', + variation: 'variation-1', + timestamp: new Date().toISOString(), + subjectAttributes: {}, + format: 'json', + evaluationDetails: null, + }; + + // Act + logger.logAssignment(assignmentEvent); + + // Assert + expect(mockEppoClient.track).not.toHaveBeenCalled(); + }); + + it('should skip tracking when entityId is undefined', () => { + // Arrange + const assignmentEvent: IAssignmentEvent = { + subject: 'user-123', + experiment: 'experiment-abc', + variant: 'control', + allocation: 'allocation-1', + featureFlag: 'feature-flag-1', + variation: 'variation-1', + timestamp: new Date().toISOString(), + subjectAttributes: {}, + format: 'json', + evaluationDetails: null, + }; + + // Act + logger.logAssignment(assignmentEvent); + + // Assert + expect(mockEppoClient.track).not.toHaveBeenCalled(); + }); }); diff --git a/src/eppo-assignment-logger.ts b/src/eppo-assignment-logger.ts index c47e999..553e8a7 100644 --- a/src/eppo-assignment-logger.ts +++ b/src/eppo-assignment-logger.ts @@ -12,11 +12,18 @@ export class EppoAssignmentLogger implements IAssignmentLogger { const { entityId: entity_id, experiment, + // holdout and holdout_variant come from `extraLogging` in FlagEvaluation holdoutKey: holdout, holdoutVariation: holdout_variant, subject: subject_id, variant, } = event; + + // Skip tracking if no entityId + if (!entity_id) { + return; + } + const payload = { entity_id, experiment, From ec4ac26e22660905e901f0547624eb815ef3de0d Mon Sep 17 00:00:00 2001 From: Felipe Lima Date: Thu, 6 Mar 2025 09:30:23 -0800 Subject: [PATCH 05/10] Update src/eppo-assignment-logger.ts Co-authored-by: Eric Petzel --- src/eppo-assignment-logger.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/eppo-assignment-logger.ts b/src/eppo-assignment-logger.ts index 553e8a7..d4d9312 100644 --- a/src/eppo-assignment-logger.ts +++ b/src/eppo-assignment-logger.ts @@ -16,7 +16,7 @@ export class EppoAssignmentLogger implements IAssignmentLogger { holdoutKey: holdout, holdoutVariation: holdout_variant, subject: subject_id, - variant, + variation, } = event; // Skip tracking if no entityId From 4391d5285c4a055de6bfd939da6dc05077cc634f Mon Sep 17 00:00:00 2001 From: Felipe Lima Date: Thu, 6 Mar 2025 09:31:10 -0800 Subject: [PATCH 06/10] Update src/eppo-assignment-logger.ts Co-authored-by: Eric Petzel --- src/eppo-assignment-logger.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/eppo-assignment-logger.ts b/src/eppo-assignment-logger.ts index d4d9312..08aed91 100644 --- a/src/eppo-assignment-logger.ts +++ b/src/eppo-assignment-logger.ts @@ -15,7 +15,7 @@ export class EppoAssignmentLogger implements IAssignmentLogger { // holdout and holdout_variant come from `extraLogging` in FlagEvaluation holdoutKey: holdout, holdoutVariation: holdout_variant, - subject: subject_id, + subject, variation, } = event; From d3f78daafc5f8b8a23c83c863b27d46d87d8e793 Mon Sep 17 00:00:00 2001 From: Felipe Lima Date: Thu, 6 Mar 2025 09:31:18 -0800 Subject: [PATCH 07/10] Update src/eppo-assignment-logger.ts Co-authored-by: Eric Petzel --- src/eppo-assignment-logger.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/eppo-assignment-logger.ts b/src/eppo-assignment-logger.ts index 08aed91..85c5a93 100644 --- a/src/eppo-assignment-logger.ts +++ b/src/eppo-assignment-logger.ts @@ -14,7 +14,7 @@ export class EppoAssignmentLogger implements IAssignmentLogger { experiment, // holdout and holdout_variant come from `extraLogging` in FlagEvaluation holdoutKey: holdout, - holdoutVariation: holdout_variant, + holdoutVariation: holdout_variation, subject, variation, } = event; From bcfbbd80427d343c1283529f754a14b379bfbea8 Mon Sep 17 00:00:00 2001 From: Felipe Lima Date: Thu, 6 Mar 2025 10:00:53 -0800 Subject: [PATCH 08/10] fix keys --- src/eppo-assignment-logger.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/eppo-assignment-logger.ts b/src/eppo-assignment-logger.ts index 85c5a93..4711b98 100644 --- a/src/eppo-assignment-logger.ts +++ b/src/eppo-assignment-logger.ts @@ -27,10 +27,10 @@ export class EppoAssignmentLogger implements IAssignmentLogger { const payload = { entity_id, experiment, - holdout_variant, + holdout_variation, holdout, - subject_id, - variant, + subject, + variation, }; this.eppoClient.track('__eppo_assignment', payload); } From 462d1109132a7993181e67f159a4f71a33a03f0e Mon Sep 17 00:00:00 2001 From: Felipe Lima Date: Thu, 6 Mar 2025 10:06:44 -0800 Subject: [PATCH 09/10] fix tests --- src/eppo-assignment-logger.spec.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/eppo-assignment-logger.spec.ts b/src/eppo-assignment-logger.spec.ts index b1cb84a..734253a 100644 --- a/src/eppo-assignment-logger.spec.ts +++ b/src/eppo-assignment-logger.spec.ts @@ -43,12 +43,12 @@ describe('EppoAssignmentLogger', () => { // Assert expect(mockEppoClient.track).toHaveBeenCalledTimes(1); expect(mockEppoClient.track).toHaveBeenCalledWith('__eppo_assignment', { - subject_id: 'user-123', + subject: 'user-123', experiment: 'experiment-abc', - variant: 'control', + variation: 'variation-1', entity_id: 456, holdout: 'holdout-xyz', - holdout_variant: 'holdout-variant-1', + holdout_variation: 'holdout-variant-1', }); }); @@ -57,7 +57,6 @@ describe('EppoAssignmentLogger', () => { const assignmentEvent: IAssignmentEvent = { subject: 'user-123', experiment: 'experiment-abc', - variant: 'control', entityId: 789, allocation: 'allocation-1', featureFlag: 'feature-flag-1', @@ -74,12 +73,12 @@ describe('EppoAssignmentLogger', () => { // Assert expect(mockEppoClient.track).toHaveBeenCalledTimes(1); expect(mockEppoClient.track).toHaveBeenCalledWith('__eppo_assignment', { - subject_id: 'user-123', + subject: 'user-123', experiment: 'experiment-abc', - variant: 'control', + variation: 'variation-1', entity_id: 789, holdout: undefined, - holdout_variant: undefined, + holdout_variation: undefined, }); }); From 2565794ffe7515bf9fb6eae1e749d451ad48d8ec Mon Sep 17 00:00:00 2001 From: Felipe Lima Date: Thu, 6 Mar 2025 10:08:38 -0800 Subject: [PATCH 10/10] export EppoAssignmentLogger --- src/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/index.ts b/src/index.ts index df1770b..6b15bc9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -40,6 +40,7 @@ import { } from './configuration-wire-types'; import * as constants from './constants'; import { decodePrecomputedFlag } from './decoding'; +import { EppoAssignmentLogger } from './eppo-assignment-logger'; import BatchEventProcessor from './events/batch-event-processor'; import { BoundedEventQueue } from './events/bounded-event-queue'; import DefaultEventDispatcher, { @@ -82,6 +83,7 @@ export { IAssignmentDetails, IAssignmentHooks, IAssignmentLogger, + EppoAssignmentLogger, IAssignmentEvent, IBanditLogger, IBanditEvent,