Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
matrix:
variations: [
{os: ubuntu-latest, node: latest},
{os: ubuntu-latest, node: 14},
{os: ubuntu-latest, node: 16},
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OpenFeature SDK requires node 16.

{os: windows-latest, node: latest}
]

Expand Down
32 changes: 24 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,30 +14,46 @@ This provider is a beta version and should not be considered ready for productio

## Supported Node versions

This version of the LaunchDarkly OpenFeature provider is compatible with Node.js versions 14 and above.
This version of the LaunchDarkly OpenFeature provider is compatible with Node.js versions 16 and above.

## Getting started

### Installation

```
npm install @openfeature/js-sdk
npm install @openfeature/server-sdk
npm install @launchdarkly/node-server-sdk
npm install @launchdarkly/openfeature-node-server
```

### Usage
```
import { OpenFeature } from '@openfeature/js-sdk';
import { OpenFeature } from '@openfeature/server-sdk';
import { init } from '@launchdarkly/node-server-sdk';
import { LaunchDarklyProvider } from '@launchdarkly/openfeature-node-server';

const ldProvider = new LaunchDarklyProvider('<your-sdk-key>', {/* LDOptions here */});
OpenFeature.setProvider(ldProvider);

const ldClient = init('<your-sdk-key>');
await ldClient.waitForInitialization();
OpenFeature.setProvider(new LaunchDarklyProvider(ldClient));
const client = OpenFeature.getClient();
const value = await client.getBooleanValue('app-enabled', false, {targetingKey: 'my-key'});
// If you need access to the LDClient, then you can use ldProvider.getClient()

// Evaluations before the provider indicates it is ready may get default values with a
// CLIENT_NOT_READY reason.
OpenFeature.addHandler(ProviderEvents.Ready, (eventDetails) => {
const client = OpenFeature.getClient();
const value = await client.getBooleanValue('app-enabled', false, {targetingKey: 'my-key'});
});

// The LaunchDarkly provider supports the ProviderEvents.ConfigurationChanged event.
// The provider will emit this event for any flag key that may have changed (each event will contain
// a single key in the `flagsChanged` field).
OpenFeature.addHandler(ProviderEvents.Ready, (eventDetails) => {
console.log(`Changed ${eventDetails.flagsChanged}`);
});

// When the LaunchDarkly provider is closed it will flush the events on the LDClient instance.
// This can be useful for short lived processes.
await OpenFeature.close();
```

Refer to the [SDK reference guide](https://docs.launchdarkly.com/sdk/server-side/node-js) for instructions on getting started with using the SDK.
Expand Down
71 changes: 65 additions & 6 deletions __tests__/LaunchDarklyProvider.test.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,85 @@
import { OpenFeature, Client, ErrorCode } from '@openfeature/js-sdk';
import { LDClient } from '@launchdarkly/node-server-sdk';
import {
OpenFeature, Client, ErrorCode, ProviderStatus, ProviderEvents,
} from '@openfeature/server-sdk';
import {
LDClient, LDClientContext, integrations,
} from '@launchdarkly/node-server-sdk';
import { LaunchDarklyProvider } from '../src';
import translateContext from '../src/translateContext';
import TestLogger from './TestLogger';

const basicContext = { targetingKey: 'the-key' };
const testFlagKey = 'a-key';

it('can be initialized', async () => {
const ldProvider = new LaunchDarklyProvider('sdk-key', { offline: true });
await ldProvider.initialize({});

expect(ldProvider.status).toEqual(ProviderStatus.READY);
await ldProvider.onClose();
});

it('can fail to initialize client', async () => {
const ldProvider = new LaunchDarklyProvider('sdk-key', {
updateProcessor: (
clientContext: LDClientContext,
dataSourceUpdates: any,
initSuccessHandler: VoidFunction,
errorHandler?: (e: Error) => void,
) => ({
start: () => {
setTimeout(() => errorHandler?.({ code: 401 } as any), 20);
},
}),
sendEvents: false,
});
try {
await ldProvider.initialize({});
} catch (e) {
expect((e as Error).message).toEqual('Authentication failed. Double check your SDK key.');
}
expect(ldProvider.status).toEqual(ProviderStatus.ERROR);
});

it('emits events for flag changes', async () => {
const td = new integrations.TestData();
const ldProvider = new LaunchDarklyProvider('sdk-key', {
updateProcessor: td.getFactory(),
sendEvents: false,
});
let count = 0;
ldProvider.events.addHandler(ProviderEvents.ConfigurationChanged, (eventDetail) => {
expect(eventDetail?.flagsChanged).toEqual(['flagA']);
count += 1;
});
td.update(td.flag('flagA').valueForAll('B'));
expect(await ldProvider.getClient()
.stringVariation('flagA', { key: 'test-key' }, 'A'))
.toEqual('B');
expect(count).toEqual(1);
await ldProvider.onClose();
});

describe('given a mock LaunchDarkly client', () => {
let ldClient: LDClient;
let ofClient: Client;
let ldProvider: LaunchDarklyProvider;
const logger: TestLogger = new TestLogger();

beforeEach(() => {
ldClient = {
variationDetail: jest.fn(),
} as any;
OpenFeature.setProvider(new LaunchDarklyProvider(ldClient, { logger }));
ldProvider = new LaunchDarklyProvider('sdk-key', { logger, offline: true });
ldClient = ldProvider.getClient();
OpenFeature.setProvider(ldProvider);

ofClient = OpenFeature.getClient();
logger.reset();
});

afterEach(async () => {
await ldProvider.onClose();
jest.restoreAllMocks();
});

it('calls the client correctly for boolean variations', async () => {
ldClient.variationDetail = jest.fn(async () => ({
value: true,
Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@
],
"license": "Apache-2.0",
"peerDependencies": {
"@openfeature/js-sdk": "^1.0.0",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The OpenFeature SDK was renamed to make it clear that their is a client and server version.

"@launchdarkly/node-server-sdk": "8.x"
"@openfeature/server-sdk": "^1.6.3",
"@launchdarkly/node-server-sdk": "9.x"
},
"devDependencies": {
"@openfeature/js-sdk": "^1.0.0",
"@openfeature/server-sdk": "^1.6.3",
"@types/jest": "^27.4.1",
"@typescript-eslint/eslint-plugin": "^5.22.0",
"@typescript-eslint/parser": "^5.22.0",
Expand All @@ -34,7 +34,7 @@
"eslint-plugin-import": "^2.26.0",
"jest": "^27.5.1",
"jest-junit": "^14.0.1",
"@launchdarkly/node-server-sdk": "8.x",
"@launchdarkly/node-server-sdk": "9.x",
"ts-jest": "^27.1.4",
"typescript": "^4.7.4"
}
Expand Down
83 changes: 77 additions & 6 deletions src/LaunchDarklyProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,20 @@ import {
ErrorCode,
EvaluationContext, FlagValue, Hook,
JsonValue,
Provider, ProviderMetadata, ResolutionDetails, StandardResolutionReasons,
} from '@openfeature/js-sdk';
OpenFeatureEventEmitter,
Provider,
ProviderEvents,
ProviderMetadata,
ProviderStatus,
ResolutionDetails,
StandardResolutionReasons,
} from '@openfeature/server-sdk';
import {
basicLogger, LDClient, LDLogger,
basicLogger, init, LDClient, LDLogger, LDOptions,
} from '@launchdarkly/node-server-sdk';
import { LaunchDarklyProviderOptions } from './LaunchDarklyProviderOptions';
import translateContext from './translateContext';
import translateResult from './translateResult';
import SafeLogger from './SafeLogger';

/**
* Create a ResolutionDetails for an evaluation that produced a type different
Expand All @@ -31,20 +37,67 @@ function wrongTypeResult<T>(value: T): ResolutionDetails<T> {
export default class LaunchDarklyProvider implements Provider {
private readonly logger: LDLogger;

private readonly client: LDClient;

private readonly clientConstructionError: any;

readonly metadata: ProviderMetadata = {
name: 'launchdarkly-node-provider',
};

private innerStatus: ProviderStatus = ProviderStatus.NOT_READY;

public readonly events = new OpenFeatureEventEmitter();

/**
* Get the status of the LaunchDarkly provider.
*/
public get status() {
return this.innerStatus;
}

/**
* Construct a {@link LaunchDarklyProvider}.
* @param client The LaunchDarkly client instance to use.
*/
constructor(private readonly client: LDClient, options: LaunchDarklyProviderOptions = {}) {
constructor(sdkKey: string, options: LDOptions = {}) {
if (options.logger) {
this.logger = options.logger;
this.logger = new SafeLogger(options.logger, basicLogger({ level: 'info' }));
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just a fix, it should have had this already.

} else {
this.logger = basicLogger({ level: 'info' });
}
try {
this.client = init(sdkKey, {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a few cases where the constructor fails. I am capturing it and letting the OF SDK handle it. That seems most in the spirit of the functionality. Maybe a little less convenient though.

...options,
wrapperName: 'open-feature/node-server',
// The wrapper version should be kept on its own line to allow easy updates using
// release-please.
wrapperVersion: '0.4.0', // x-release-please-version
});
this.client.on('update', ({ key }: { key: string }) => this.events.emit(ProviderEvents.ConfigurationChanged, { flagsChanged: [key] }));
} catch (e) {
this.clientConstructionError = e;
this.logger.error(`Encountered unrecoverable initialization error, ${e}`);
this.innerStatus = ProviderStatus.ERROR;
}
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
async initialize(context?: EvaluationContext): Promise<void> {
if (!this.client) {
// The client could not be constructed.
if (this.clientConstructionError) {
throw this.clientConstructionError;
}
throw new Error('Unknown problem encountered during initialization');
}
try {
await this.client.waitForInitialization();
this.innerStatus = ProviderStatus.READY;
} catch (e) {
this.innerStatus = ProviderStatus.ERROR;
throw e;
}
}

/**
Expand Down Expand Up @@ -169,4 +222,22 @@ export default class LaunchDarklyProvider implements Provider {
private translateContext(context: EvaluationContext) {
return translateContext(this.logger, context);
}

/**
* Get the LDClient instance used by this provider.
*
* @returns The client for this provider.
*/
public getClient(): LDClient {
return this.client;
}

/**
* Called by OpenFeature when it needs to close the provider. This will flush
* events from the LDClient and then close it.
*/
async onClose(): Promise<void> {
await this.client.flush();
this.client.close();
}
}
23 changes: 0 additions & 23 deletions src/LaunchDarklyProviderOptions.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/translateContext.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { EvaluationContext, EvaluationContextValue } from '@openfeature/js-sdk';
import { EvaluationContext, EvaluationContextValue } from '@openfeature/server-sdk';
import {
LDContext, LDContextCommon, LDLogger, LDSingleKindContext,
} from '@launchdarkly/node-server-sdk';
Expand Down
2 changes: 1 addition & 1 deletion src/translateResult.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ErrorCode, ResolutionDetails } from '@openfeature/js-sdk';
import { ErrorCode, ResolutionDetails } from '@openfeature/server-sdk';
import { LDEvaluationDetail } from '@launchdarkly/node-server-sdk';

/**
Expand Down
Loading