Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions packages/sdk/openfeature-node-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,24 @@ There are several attributes with special handling within a single or multi-cont
- `anonymous` - Must be a boolean. Equivalent to `anonymous` in the SDK.
- `name` - Must be a string. Equivalent to `name` in the SDK.

### Flag Metadata

Evaluation results include LaunchDarkly specific information in the OpenFeature flag metadata. A key is only present when it applies to the evaluation.

| Key | Type | Description |
|---|---|---|
| `variationIndex` | number | The index of the served variation. Absent when the default value was returned. |
| `inExperiment` | boolean | Present, and `true`, when the evaluation was part of an experiment. |
| `ruleIndex` | number | The index of the matched rule, for a `RULE_MATCH` reason. |
| `ruleId` | string | The identifier of the matched rule, for a `RULE_MATCH` reason. |
| `prerequisiteKey` | string | The key of the failed prerequisite flag, for a `PREREQUISITE_FAILED` reason. |
| `bigSegmentsStatus` | string | The status of the Big Segment query, when the evaluation required one. |

```typescript
const details = await client.getBooleanDetails('my-boolean-flag', false);
const inExperiment = details.flagMetadata.inExperiment === true;
```

### Examples

#### A single user context
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,12 @@ it('resolveBooleanEvaluation returns the translated result for boolean flags', a
{ kind: 'user', key: 'u' },
false,
);
expect(result).toEqual({ value: true, variant: '1', reason: 'OFF' });
expect(result).toEqual({
value: true,
variant: '1',
reason: 'OFF',
flagMetadata: { variationIndex: 1 },
});
});

it('resolveStringEvaluation returns the translated result for string flags', async () => {
Expand All @@ -97,7 +102,12 @@ it('resolveStringEvaluation returns the translated result for string flags', asy

const result = await provider.resolveStringEvaluation('flag', 'red', { targetingKey: 'u' });

expect(result).toEqual({ value: 'green', variant: '0', reason: 'FALLTHROUGH' });
expect(result).toEqual({
value: 'green',
variant: '0',
reason: 'FALLTHROUGH',
flagMetadata: { variationIndex: 0 },
});
});

it('resolveNumberEvaluation returns the translated result for number flags', async () => {
Expand All @@ -111,7 +121,12 @@ it('resolveNumberEvaluation returns the translated result for number flags', asy

const result = await provider.resolveNumberEvaluation('flag', 0, { targetingKey: 'u' });

expect(result).toEqual({ value: 42, variant: '2', reason: 'TARGET_MATCH' });
expect(result).toEqual({
value: 42,
variant: '2',
reason: 'TARGET_MATCH',
flagMetadata: { variationIndex: 2 },
});
});

it('resolveObjectEvaluation returns the translated result for object flags', async () => {
Expand All @@ -129,7 +144,12 @@ it('resolveObjectEvaluation returns the translated result for object flags', asy
{ targetingKey: 'u' },
);

expect(result).toEqual({ value: { a: 1 }, variant: '3', reason: 'RULE_MATCH' });
expect(result).toEqual({
value: { a: 1 },
variant: '3',
reason: 'RULE_MATCH',
flagMetadata: { variationIndex: 3 },
});
});

it.each([
Expand Down Expand Up @@ -328,6 +348,11 @@ it('wraps a host logger that throws so flag evaluation does not crash', async ()
targetingKey: 'u',
kind: 42 as unknown as string,
}),
).resolves.toEqual({ value: true, variant: '0', reason: 'OFF' });
).resolves.toEqual({
value: true,
variant: '0',
reason: 'OFF',
flagMetadata: { variationIndex: 0 },
});
});

Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,73 @@ it('does populate the errorCode when there is an error', () => {
});
expect(translated.errorCode).toEqual('GENERAL');
});

it('includes the variation index in the flag metadata', () => {
expect(
translateResult<boolean>({
value: true,
variationIndex: 9,
reason: { kind: 'FALLTHROUGH' },
}).flagMetadata,
).toEqual({ variationIndex: 9 });
});

it('omits the variation index from the flag metadata when there is no variation', () => {
expect(
translateResult<boolean>({
value: true,
variationIndex: null,
reason: { kind: 'ERROR', errorKind: 'FLAG_NOT_FOUND' },
}).flagMetadata,
).toEqual({});
});

it('includes inExperiment in the flag metadata for experiment evaluations', () => {
expect(
translateResult<boolean>({
value: true,
variationIndex: 9,
reason: { kind: 'FALLTHROUGH', inExperiment: true },
}).flagMetadata,
).toEqual({ variationIndex: 9, inExperiment: true });
});

it('omits inExperiment from the flag metadata for non-experiment evaluations', () => {
expect(
translateResult<boolean>({
value: true,
variationIndex: 9,
reason: { kind: 'FALLTHROUGH', inExperiment: false },
}).flagMetadata,
).toEqual({ variationIndex: 9 });
});

it('includes the rule in the flag metadata for rule matches', () => {
expect(
translateResult<boolean>({
value: true,
variationIndex: 9,
reason: { kind: 'RULE_MATCH', ruleIndex: 2, ruleId: 'the-rule-id' },
}).flagMetadata,
).toEqual({ variationIndex: 9, ruleIndex: 2, ruleId: 'the-rule-id' });
});

it('includes the prerequisite key in the flag metadata for failed prerequisites', () => {
expect(
translateResult<boolean>({
value: true,
variationIndex: 9,
reason: { kind: 'PREREQUISITE_FAILED', prerequisiteKey: 'the-prerequisite-key' },
}).flagMetadata,
).toEqual({ variationIndex: 9, prerequisiteKey: 'the-prerequisite-key' });
});

it('includes the big segments status in the flag metadata', () => {
expect(
translateResult<boolean>({
value: true,
variationIndex: 9,
reason: { kind: 'FALLTHROUGH', bigSegmentsStatus: 'STALE' },
}).flagMetadata,
).toEqual({ variationIndex: 9, bigSegmentsStatus: 'STALE' });
});
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { ErrorCode } from '@openfeature/server-sdk';
import type { ResolutionDetails } from '@openfeature/server-sdk';
import type { FlagMetadata, ResolutionDetails } from '@openfeature/server-sdk';

import type { LDEvaluationDetail } from '@launchdarkly/js-sdk-common';

const VARIATION_INDEX_KEY = 'variationIndex';
const IN_EXPERIMENT_KEY = 'inExperiment';
const RULE_INDEX_KEY = 'ruleIndex';
const RULE_ID_KEY = 'ruleId';
const PREREQUISITE_KEY_KEY = 'prerequisiteKey';
const BIG_SEGMENTS_STATUS_KEY = 'bigSegmentsStatus';

/**
* Convert an `errorKind` into an OpenFeature `errorCode`.
*/
Expand All @@ -21,6 +28,32 @@ function translateErrorKind(errorKind: string | undefined): ErrorCode {
}
}

/**
* Convert the LaunchDarkly specific parts of an evaluation into OpenFeature flag metadata.
*/
function translateFlagMetadata(result: LDEvaluationDetail): FlagMetadata {
const metadata: FlagMetadata = {};
if (result.variationIndex !== undefined && result.variationIndex !== null) {
metadata[VARIATION_INDEX_KEY] = result.variationIndex;
}
if (result.reason.inExperiment) {
metadata[IN_EXPERIMENT_KEY] = true;
}
if (result.reason.ruleIndex !== undefined) {
metadata[RULE_INDEX_KEY] = result.reason.ruleIndex;
}
if (result.reason.ruleId !== undefined) {
metadata[RULE_ID_KEY] = result.reason.ruleId;
}
if (result.reason.prerequisiteKey !== undefined) {
metadata[PREREQUISITE_KEY_KEY] = result.reason.prerequisiteKey;
}
if (result.reason.bigSegmentsStatus !== undefined) {
metadata[BIG_SEGMENTS_STATUS_KEY] = result.reason.bigSegmentsStatus;
}
return metadata;
}

/**
* Translate an {@link LDEvaluationDetail} to a {@link ResolutionDetails}.
*
Expand All @@ -30,6 +63,7 @@ export function translateResult<T>(result: LDEvaluationDetail): ResolutionDetail
value: result.value,
variant: result.variationIndex?.toString(),
reason: result.reason.kind,
flagMetadata: translateFlagMetadata(result),
};

if (result.reason.kind === 'ERROR') {
Expand Down
Loading