Skip to content

Commit fdeba09

Browse files
Merge pull request #11 from splitio/fme-9851
Add track and evaluation with details
2 parents 0922be2 + a4ed435 commit fdeba09

File tree

5 files changed

+136
-8
lines changed

5 files changed

+136
-8
lines changed

src/__tests__/node.spec.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import tape from 'tape-catch';
22

33
import clientSuite from './nodeSuites/client.spec.js';
4+
import providerSuite from './nodeSuites/provider.spec.js';
45

5-
tape('## OpenFeature JavaScript Split Provider - tests', async function (assert) {
6+
tape('## OpenFeature JavaScript Split Client - tests', async function (assert) {
67
assert.test('Client Tests', clientSuite);
7-
});
8+
});
9+
10+
11+
tape('## OpenFeature JavaScript Split Provider - tests', async function (assert) {
12+
assert.test('Provider Tests', providerSuite);
13+
});

src/__tests__/nodeSuites/client.spec.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,11 @@ export default async function(assert) {
4949
const getBooleanSplitWithKeyTest = async (client) => {
5050
let result = await client.getBooleanDetails('my_feature', false);
5151
assert.equals(result.value, true);
52-
assert.looseEquals(result.flagMetadata, { desc: 'this applies only to ON treatment' });
52+
assert.looseEquals(result.flagMetadata, { config: '{"desc" : "this applies only to ON treatment"}' });
5353

5454
result = await client.getBooleanDetails('my_feature', true, { targetingKey: 'randomKey' });
5555
assert.equals(result.value, false);
56-
assert.looseEquals(result.flagMetadata, {});
56+
assert.looseEquals(result.flagMetadata, { config: ''});
5757
};
5858

5959
const getStringSplitTest = async (client) => {

src/__tests__/nodeSuites/provider.spec.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import { ParseError } from "@openfeature/server-sdk";
2+
import { makeProviderWithSpy } from "../testUtils";
3+
14
export default async function(assert) {
25

36
const shouldFailWithBadApiKeyTest = () => {
@@ -80,6 +83,63 @@ export default async function(assert) {
8083
assert.equal(1, 1);
8184
};
8285

86+
const trackingSuite = (t) => {
87+
88+
t.test("track: throws when missing eventName", async (t) => {
89+
const { provider } = makeProviderWithSpy();
90+
try {
91+
await provider.track("", { targetingKey: "u1", trafficType: "user" }, {});
92+
t.fail("expected ParseError for eventName");
93+
} catch (e) {
94+
t.ok(e instanceof ParseError, "got ParseError");
95+
}
96+
t.end();
97+
});
98+
99+
t.test("track: throws when missing trafficType", async (t) => {
100+
const { provider } = makeProviderWithSpy();
101+
try {
102+
await provider.track("evt", { targetingKey: "u1" }, {});
103+
t.fail("expected ParseError for trafficType");
104+
} catch (e) {
105+
t.ok(e instanceof ParseError, "got ParseError");
106+
}
107+
t.end();
108+
});
109+
110+
t.test("track: ok without details", async (t) => {
111+
const { provider, calls } = makeProviderWithSpy();
112+
await provider.track("view", { targetingKey: "u1", trafficType: "user" }, null);
113+
114+
t.equal(calls.count, 1, "Split track called once");
115+
t.deepEqual(
116+
calls.args,
117+
["u1", "user", "view", undefined, {}],
118+
"called with key, trafficType, eventName, 0, {}"
119+
);
120+
t.end();
121+
});
122+
123+
t.test("track: ok with details", async (t) => {
124+
const { provider, calls } = makeProviderWithSpy();
125+
await provider.track(
126+
"purchase",
127+
{ targetingKey: "u1", trafficType: "user" },
128+
{ value: 9.99, properties: { plan: "pro", beta: true } }
129+
);
130+
131+
t.equal(calls.count, 1, "Split track called once");
132+
t.equal(calls.args[0], "u1");
133+
t.equal(calls.args[1], "user");
134+
t.equal(calls.args[2], "purchase");
135+
t.equal(calls.args[3], 9.99);
136+
t.deepEqual(calls.args[4], { plan: "pro", beta: true });
137+
t.end();
138+
});
139+
}
140+
141+
trackingSuite(assert);
142+
83143
shouldFailWithBadApiKeyTest();
84144

85145
evalBooleanNullEmptyTest();

src/__tests__/testUtils/index.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { OpenFeatureSplitProvider } from "../..";
2+
13
const DEFAULT_ERROR_MARGIN = 50; // 0.05 secs
24

35
/**
@@ -67,3 +69,23 @@ export function url(settings, target) {
6769
}
6870
return `${settings.urls.sdk}${target}`;
6971
}
72+
73+
74+
/**
75+
* Create a spy for the OpenFeatureSplitProvider.
76+
* @returns {provider: OpenFeatureSplitProvider, calls: {count: number, args: any[]}}
77+
*/
78+
export function makeProviderWithSpy() {
79+
const calls = { count: 0, args: null };
80+
const track = (...args) => { calls.count++; calls.args = args; return true; };
81+
82+
const splitClient = {
83+
__getStatus: () => ({ isReady: true }),
84+
on: () => {},
85+
Event: { SDK_READY: "SDK_READY" },
86+
track,
87+
getTreatmentWithConfig: () => ({ treatment: "on", config: "" }),
88+
};
89+
90+
return { provider: new OpenFeatureSplitProvider({ splitClient }), calls };
91+
}

src/lib/js-split-provider.ts

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
JsonValue,
88
TargetingKeyMissingError,
99
StandardResolutionReasons,
10+
TrackingEventDetails,
1011
} from "@openfeature/server-sdk";
1112
import type SplitIO from "@splitsoftware/splitio/types/splitio";
1213

@@ -53,16 +54,17 @@ export class OpenFeatureSplitProvider implements Provider {
5354
flagKey,
5455
this.transformContext(context)
5556
);
57+
const treatment = details.value.toLowerCase();
5658

57-
if ( details.value === "on" || details.value === "true" ) {
59+
if ( treatment === "on" || treatment === "true" ) {
5860
return { ...details, value: true };
5961
}
6062

61-
if ( details.value === "off" || details.value === "false" ) {
63+
if ( treatment === "off" || treatment === "false" ) {
6264
return { ...details, value: false };
6365
}
6466

65-
throw new ParseError(`Invalid boolean value for ${details.value}`);
67+
throw new ParseError(`Invalid boolean value for ${treatment}`);
6668
}
6769

6870
async resolveStringEvaluation(
@@ -119,7 +121,7 @@ export class OpenFeatureSplitProvider implements Provider {
119121
if (value === CONTROL_TREATMENT) {
120122
throw new FlagNotFoundError(CONTROL_VALUE_ERROR_MESSAGE);
121123
}
122-
const flagMetadata = config ? JSON.parse(config) : undefined;
124+
const flagMetadata = { config: config ? config : '' };
123125
const details: ResolutionDetails<string> = {
124126
value: value,
125127
variant: value,
@@ -130,6 +132,44 @@ export class OpenFeatureSplitProvider implements Provider {
130132
}
131133
}
132134

135+
async track(
136+
trackingEventName: string,
137+
context: EvaluationContext,
138+
details: TrackingEventDetails
139+
): Promise<void> {
140+
141+
// targetingKey is always required
142+
const { targetingKey } = context;
143+
if (targetingKey == null || targetingKey === "")
144+
throw new TargetingKeyMissingError();
145+
146+
// eventName is always required
147+
if (trackingEventName == null || trackingEventName === "")
148+
throw new ParseError("Missing eventName, required to track");
149+
150+
// trafficType is always required
151+
const ttVal = context["trafficType"];
152+
const trafficType =
153+
ttVal != null && typeof ttVal === "string" && ttVal.trim() !== ""
154+
? ttVal
155+
: null;
156+
if (trafficType == null || trafficType === "")
157+
throw new ParseError("Missing trafficType variable, required to track");
158+
159+
let value;
160+
let properties: SplitIO.Properties = {};
161+
if (details != null) {
162+
if (details.value != null) {
163+
value = details.value;
164+
}
165+
if (details.properties != null) {
166+
properties = details.properties as SplitIO.Properties;
167+
}
168+
}
169+
170+
this.client.track(targetingKey, trafficType, trackingEventName, value, properties);
171+
}
172+
133173
//Transform the context into an object useful for the Split API, an key string with arbitrary Split "Attributes".
134174
private transformContext(context: EvaluationContext): Consumer {
135175
const { targetingKey, ...attributes } = context;

0 commit comments

Comments
 (0)