From fa3a02680f1d27218da85a43108fe427f9f6831e Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Tue, 2 Sep 2025 17:44:04 -0300 Subject: [PATCH 1/2] Add track and evaluation with details --- src/__tests__/node.spec.js | 10 +++- src/__tests__/nodeSuites/client.spec.js | 4 +- src/__tests__/nodeSuites/provider.spec.js | 60 +++++++++++++++++++++++ src/__tests__/testUtils/index.js | 22 +++++++++ src/lib/js-split-provider.ts | 48 ++++++++++++++++-- 5 files changed, 136 insertions(+), 8 deletions(-) diff --git a/src/__tests__/node.spec.js b/src/__tests__/node.spec.js index dab4e8f..a8fad03 100644 --- a/src/__tests__/node.spec.js +++ b/src/__tests__/node.spec.js @@ -1,7 +1,13 @@ import tape from 'tape-catch'; import clientSuite from './nodeSuites/client.spec.js'; +import providerSuite from './nodeSuites/provider.spec.js'; -tape('## OpenFeature JavaScript Split Provider - tests', async function (assert) { +tape('## OpenFeature JavaScript Split Client - tests', async function (assert) { assert.test('Client Tests', clientSuite); -}); \ No newline at end of file +}); + + +tape('## OpenFeature JavaScript Split Provider - tests', async function (assert) { + assert.test('Provider Tests', providerSuite); +}); diff --git a/src/__tests__/nodeSuites/client.spec.js b/src/__tests__/nodeSuites/client.spec.js index 2cdc813..01fac01 100644 --- a/src/__tests__/nodeSuites/client.spec.js +++ b/src/__tests__/nodeSuites/client.spec.js @@ -49,11 +49,11 @@ export default async function(assert) { const getBooleanSplitWithKeyTest = async (client) => { let result = await client.getBooleanDetails('my_feature', false); assert.equals(result.value, true); - assert.looseEquals(result.flagMetadata, { desc: 'this applies only to ON treatment' }); + assert.looseEquals(result.flagMetadata, { config: '{"desc" : "this applies only to ON treatment"}' }); result = await client.getBooleanDetails('my_feature', true, { targetingKey: 'randomKey' }); assert.equals(result.value, false); - assert.looseEquals(result.flagMetadata, {}); + assert.looseEquals(result.flagMetadata, { config: ''}); }; const getStringSplitTest = async (client) => { diff --git a/src/__tests__/nodeSuites/provider.spec.js b/src/__tests__/nodeSuites/provider.spec.js index f032947..97d0509 100644 --- a/src/__tests__/nodeSuites/provider.spec.js +++ b/src/__tests__/nodeSuites/provider.spec.js @@ -1,3 +1,6 @@ +import { ParseError } from "@openfeature/server-sdk"; +import { makeProviderWithSpy } from "../testUtils"; + export default async function(assert) { const shouldFailWithBadApiKeyTest = () => { @@ -80,6 +83,63 @@ export default async function(assert) { assert.equal(1, 1); }; + const trackingSuite = (t) => { + + t.test("track: throws when missing eventName", async (t) => { + const { provider } = makeProviderWithSpy(); + try { + await provider.track("", { targetingKey: "u1", trafficType: "user" }, {}); + t.fail("expected ParseError for eventName"); + } catch (e) { + t.ok(e instanceof ParseError, "got ParseError"); + } + t.end(); + }); + + t.test("track: throws when missing trafficType", async (t) => { + const { provider } = makeProviderWithSpy(); + try { + await provider.track("evt", { targetingKey: "u1" }, {}); + t.fail("expected ParseError for trafficType"); + } catch (e) { + t.ok(e instanceof ParseError, "got ParseError"); + } + t.end(); + }); + + t.test("track: ok without details", async (t) => { + const { provider, calls } = makeProviderWithSpy(); + await provider.track("view", { targetingKey: "u1", trafficType: "user" }, null); + + t.equal(calls.count, 1, "Split track called once"); + t.deepEqual( + calls.args, + ["u1", "user", "view", 0, {}], + "called with key, trafficType, eventName, 0, {}" + ); + t.end(); + }); + + t.test("track: ok with details", async (t) => { + const { provider, calls } = makeProviderWithSpy(); + await provider.track( + "purchase", + { targetingKey: "u1", trafficType: "user" }, + { value: 9.99, properties: { plan: "pro", beta: true } } + ); + + t.equal(calls.count, 1, "Split track called once"); + t.equal(calls.args[0], "u1"); + t.equal(calls.args[1], "user"); + t.equal(calls.args[2], "purchase"); + t.equal(calls.args[3], 9.99); + t.deepEqual(calls.args[4], { plan: "pro", beta: true }); + t.end(); + }); +} + + trackingSuite(assert); + shouldFailWithBadApiKeyTest(); evalBooleanNullEmptyTest(); diff --git a/src/__tests__/testUtils/index.js b/src/__tests__/testUtils/index.js index 885bcd1..70eda26 100644 --- a/src/__tests__/testUtils/index.js +++ b/src/__tests__/testUtils/index.js @@ -1,3 +1,5 @@ +import { OpenFeatureSplitProvider } from "../.."; + const DEFAULT_ERROR_MARGIN = 50; // 0.05 secs /** @@ -67,3 +69,23 @@ export function url(settings, target) { } return `${settings.urls.sdk}${target}`; } + + +/** + * Create a spy for the OpenFeatureSplitProvider. + * @returns {provider: OpenFeatureSplitProvider, calls: {count: number, args: any[]}} + */ +export function makeProviderWithSpy() { + const calls = { count: 0, args: null }; + const track = (...args) => { calls.count++; calls.args = args; return true; }; + + const splitClient = { + __getStatus: () => ({ isReady: true }), + on: () => {}, + Event: { SDK_READY: "SDK_READY" }, + track, + getTreatmentWithConfig: () => ({ treatment: "on", config: "" }), + }; + + return { provider: new OpenFeatureSplitProvider({ splitClient }), calls }; +} \ No newline at end of file diff --git a/src/lib/js-split-provider.ts b/src/lib/js-split-provider.ts index 6bcfc1b..7666d22 100644 --- a/src/lib/js-split-provider.ts +++ b/src/lib/js-split-provider.ts @@ -7,6 +7,7 @@ import { JsonValue, TargetingKeyMissingError, StandardResolutionReasons, + TrackingEventDetails, } from "@openfeature/server-sdk"; import type SplitIO from "@splitsoftware/splitio/types/splitio"; @@ -53,16 +54,17 @@ export class OpenFeatureSplitProvider implements Provider { flagKey, this.transformContext(context) ); + const flagName = details.value.toLowerCase(); - if ( details.value === "on" || details.value === "true" ) { + if ( flagName === "on" || flagName === "true" ) { return { ...details, value: true }; } - if ( details.value === "off" || details.value === "false" ) { + if ( flagName === "off" || flagName === "false" ) { return { ...details, value: false }; } - throw new ParseError(`Invalid boolean value for ${details.value}`); + throw new ParseError(`Invalid boolean value for ${flagName}`); } async resolveStringEvaluation( @@ -119,7 +121,7 @@ export class OpenFeatureSplitProvider implements Provider { if (value === CONTROL_TREATMENT) { throw new FlagNotFoundError(CONTROL_VALUE_ERROR_MESSAGE); } - const flagMetadata = config ? JSON.parse(config) : undefined; + const flagMetadata = { config: config ? config : '' }; const details: ResolutionDetails = { value: value, variant: value, @@ -130,6 +132,44 @@ export class OpenFeatureSplitProvider implements Provider { } } + async track( + trackingEventName: string, + context: EvaluationContext, + details: TrackingEventDetails + ): Promise { + + // targetingKey is always required + const { targetingKey } = context; + if (targetingKey == null || targetingKey === "") + throw new TargetingKeyMissingError(); + + // eventName is always required + if (trackingEventName == null || trackingEventName === "") + throw new ParseError("Missing eventName, required to track"); + + // trafficType is always required + const ttVal = context["trafficType"]; + const trafficType = + ttVal != null && typeof ttVal === "string" && ttVal.trim() !== "" + ? ttVal + : null; + if (trafficType == null || trafficType === "") + throw new ParseError("Missing trafficType variable, required to track"); + + let value = 0; + let properties: SplitIO.Properties = {}; + if (details != null) { + if (details.value != null) { + value = details.value; + } + if (details.properties != null) { + properties = details.properties as SplitIO.Properties; + } + } + + this.client.track(targetingKey, trafficType, trackingEventName, value, properties); + } + //Transform the context into an object useful for the Split API, an key string with arbitrary Split "Attributes". private transformContext(context: EvaluationContext): Consumer { const { targetingKey, ...attributes } = context; From a4ed435c22bcc485483cfbb7b8437ce1fe2e4c4c Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Fri, 5 Sep 2025 18:23:02 -0300 Subject: [PATCH 2/2] rename flagname to treatment --- src/__tests__/nodeSuites/provider.spec.js | 2 +- src/lib/js-split-provider.ts | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/__tests__/nodeSuites/provider.spec.js b/src/__tests__/nodeSuites/provider.spec.js index 97d0509..a0cc7a1 100644 --- a/src/__tests__/nodeSuites/provider.spec.js +++ b/src/__tests__/nodeSuites/provider.spec.js @@ -114,7 +114,7 @@ export default async function(assert) { t.equal(calls.count, 1, "Split track called once"); t.deepEqual( calls.args, - ["u1", "user", "view", 0, {}], + ["u1", "user", "view", undefined, {}], "called with key, trafficType, eventName, 0, {}" ); t.end(); diff --git a/src/lib/js-split-provider.ts b/src/lib/js-split-provider.ts index 7666d22..ff2d90b 100644 --- a/src/lib/js-split-provider.ts +++ b/src/lib/js-split-provider.ts @@ -54,17 +54,17 @@ export class OpenFeatureSplitProvider implements Provider { flagKey, this.transformContext(context) ); - const flagName = details.value.toLowerCase(); + const treatment = details.value.toLowerCase(); - if ( flagName === "on" || flagName === "true" ) { + if ( treatment === "on" || treatment === "true" ) { return { ...details, value: true }; } - if ( flagName === "off" || flagName === "false" ) { + if ( treatment === "off" || treatment === "false" ) { return { ...details, value: false }; } - throw new ParseError(`Invalid boolean value for ${flagName}`); + throw new ParseError(`Invalid boolean value for ${treatment}`); } async resolveStringEvaluation( @@ -156,7 +156,7 @@ export class OpenFeatureSplitProvider implements Provider { if (trafficType == null || trafficType === "") throw new ParseError("Missing trafficType variable, required to track"); - let value = 0; + let value; let properties: SplitIO.Properties = {}; if (details != null) { if (details.value != null) {