Skip to content

Commit 0818364

Browse files
feat: OpenFeature Tracking Support (#52)
**Requirements** - [x] I have added test coverage for new or changed functionality - [x] I have followed the repository's [pull request submission guidelines](../blob/main/CONTRIBUTING.md#submitting-pull-requests) - [x] I have validated my changes against all supported platform versions **Related issues** https://launchdarkly.atlassian.net/browse/SDK-1117 **Describe the solution you've provided** Implemented OpenFeature Provider method `track()` in LaunchDarklyProvider. It makes a call under the hood to `LDClient.track()`. The `value` attribute will be pulled out of the TrackingEventDetails param and passed to `LDClient.track()` as metricValue. The remaining object (without value) will be passed to `LDClient.track()` as data. If the remaining object is empty then undefined will be passed. **Describe alternatives you've considered** N/A **Additional context** N/A
2 parents db4db4c + 807308e commit 0818364

File tree

6 files changed

+113
-3
lines changed

6 files changed

+113
-3
lines changed

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ We encourage pull requests and other contributions from the community. Before su
1414

1515
### Prerequisites
1616

17-
The project should be built and tested against the lowest compatible version, Node 16. It uses `npm`, which is bundled in all supported versions of Node.
17+
The project should be built and tested against the lowest compatible version, Node 18. It uses `npm`, which is bundled in all supported versions of Node.
1818

1919
### Setup
2020

__tests__/LaunchDarklyProvider.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,4 +326,54 @@ describe('given a mock LaunchDarkly client', () => {
326326
expect(logger.logs[0]).toEqual("The EvaluationContext contained both a 'targetingKey' and a"
327327
+ " 'key' attribute. The 'key' attribute will be discarded.");
328328
});
329+
330+
it('handles tracking with invalid context', () => {
331+
ofClient.track('test-event', {});
332+
expect(logger.logs[0]).toEqual("The EvaluationContext must contain either a 'targetingKey' "
333+
+ "or a 'key' and the type must be a string.");
334+
});
335+
336+
it('handles tracking with no data or metricValue', () => {
337+
ldClient.track = jest.fn();
338+
ofClient.track('test-event', basicContext);
339+
expect(ldClient.track).toHaveBeenCalledWith(
340+
'test-event',
341+
translateContext(logger, basicContext),
342+
undefined,
343+
undefined,
344+
);
345+
});
346+
347+
it('handles tracking with only metricValue', () => {
348+
ldClient.track = jest.fn();
349+
ofClient.track('test-event', basicContext, { value: 12345 });
350+
expect(ldClient.track).toHaveBeenCalledWith(
351+
'test-event',
352+
translateContext(logger, basicContext),
353+
undefined,
354+
12345,
355+
);
356+
});
357+
358+
it('handles tracking with data but no metricValue', () => {
359+
ldClient.track = jest.fn();
360+
ofClient.track('test-event', basicContext, { key1: 'val1' });
361+
expect(ldClient.track).toHaveBeenCalledWith(
362+
'test-event',
363+
translateContext(logger, basicContext),
364+
{ key1: 'val1' },
365+
undefined,
366+
);
367+
});
368+
369+
it('handles tracking with data and metricValue', () => {
370+
ldClient.track = jest.fn();
371+
ofClient.track('test-event', basicContext, { value: 12345, key1: 'val1' });
372+
expect(ldClient.track).toHaveBeenCalledWith(
373+
'test-event',
374+
translateContext(logger, basicContext),
375+
{ key1: 'val1' },
376+
12345,
377+
);
378+
});
329379
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import translateTrackingEventDetails from '../src/translateTrackingEventDetails';
2+
3+
it('returns undefined if details are empty', () => {
4+
expect(translateTrackingEventDetails({})).toBeUndefined();
5+
});
6+
7+
it('returns undefined if details only contains value', () => {
8+
expect(translateTrackingEventDetails({ value: 12345 })).toBeUndefined();
9+
});
10+
11+
it('returns an object without the value attribute', () => {
12+
expect(translateTrackingEventDetails({
13+
value: 12345,
14+
key1: 'val1',
15+
key2: 'val2',
16+
})).toEqual({
17+
key1: 'val1',
18+
key2: 'val2',
19+
});
20+
});

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,11 @@
3030
"license": "Apache-2.0",
3131
"peerDependencies": {
3232
"@launchdarkly/node-server-sdk": "9.x",
33-
"@openfeature/server-sdk": "^1.14.0"
33+
"@openfeature/server-sdk": "^1.16.0"
3434
},
3535
"devDependencies": {
3636
"@launchdarkly/node-server-sdk": "9.x",
37-
"@openfeature/server-sdk": "^1.14.0",
37+
"@openfeature/server-sdk": "^1.16.0",
3838
"@types/jest": "^29.5.14",
3939
"@typescript-eslint/eslint-plugin": "^5.22.0",
4040
"@typescript-eslint/parser": "^5.22.0",

src/LaunchDarklyProvider.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ import {
99
ProviderMetadata,
1010
ResolutionDetails,
1111
StandardResolutionReasons,
12+
TrackingEventDetails,
1213
} from '@openfeature/server-sdk';
1314
import {
1415
basicLogger, init, LDClient, LDLogger, LDOptions,
1516
} from '@launchdarkly/node-server-sdk';
1617
import translateContext from './translateContext';
1718
import translateResult from './translateResult';
19+
import translateTrackingEventDetails from './translateTrackingEventDetails';
1820
import SafeLogger from './SafeLogger';
1921

2022
/**
@@ -229,4 +231,24 @@ export default class LaunchDarklyProvider implements Provider {
229231
await this.client.flush();
230232
this.client.close();
231233
}
234+
235+
/**
236+
* Track a user action or application state, usually representing a business objective or outcome.
237+
* @param trackingEventName The name of the event, which may correspond to a metric
238+
* in Experimentation.
239+
* @param context The context to track.
240+
* @param trackingEventDetails Optional additional information to associate with the event.
241+
*/
242+
track(
243+
trackingEventName: string,
244+
context: EvaluationContext,
245+
trackingEventDetails: TrackingEventDetails,
246+
): void {
247+
this.client.track(
248+
trackingEventName,
249+
this.translateContext(context),
250+
translateTrackingEventDetails(trackingEventDetails),
251+
trackingEventDetails?.value,
252+
);
253+
}
232254
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { TrackingEventDetails, TrackingEventValue } from '@openfeature/server-sdk';
2+
3+
/**
4+
* Translate {@link TrackingEventDetails} to an object suitable for use as the data
5+
* parameter in LDClient.track().
6+
* @param details The {@link TrackingEventDetails} to translate.
7+
* @returns An object suitable use as the data parameter in LDClient.track().
8+
* The value attribute will be removed and if the resulting object is empty,
9+
* returns undefined.
10+
*
11+
* @internal
12+
*/
13+
export default function translateTrackingEventDetails(
14+
details: TrackingEventDetails,
15+
): Record<string, TrackingEventValue> | undefined {
16+
const { value, ...data } = details;
17+
return Object.keys(data).length ? data : undefined;
18+
}

0 commit comments

Comments
 (0)