Skip to content

Commit

Permalink
feat: add flag metadata (#502)
Browse files Browse the repository at this point in the history
Signed-off-by: Todd Baert <todd.baert@dynatrace.com>
  • Loading branch information
toddbaert committed Jul 28, 2023
1 parent 2563949 commit c8a80c6
Show file tree
Hide file tree
Showing 4 changed files with 45 additions and 42 deletions.
36 changes: 21 additions & 15 deletions libs/providers/flagd/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Server-side flagd Provider
# Server-Side flagd Provider

Flagd is a simple daemon for evaluating feature flags.
It is designed to conform to OpenFeature schema for flag definitions.
Expand All @@ -21,19 +21,19 @@ $ npm install @openfeature/js-sdk
The `FlagdProvider` supports multiple configuration options that determine now the SDK communicates with flagd.
Options can be defined in the constructor or as environment variables, with constructor options having the highest precedence.

### Available options
### Available Options

| Option name | Environment variable name | Type | Default | Values |
| --------------------- | ------------------------------- | ------- | --------- | ------------- |
| host | FLAGD_HOST | string | localhost | |
| port | FLAGD_PORT | number | 8013 | |
| tls | FLAGD_TLS | boolean | false | |
| socketPath | FLAGD_SOCKET_PATH | string | - | |
| cache | FLAGD_CACHE | string | lru | lru,disabled |
| maxCacheSize | FLAGD_MAX_CACHE_SIZE | int | 1000 | |
| maxEventStreamRetries | FLAGD_MAX_EVENT_STREAM_RETRIES | int | 5 | |
| Option name | Environment variable name | Type | Default | Values |
| --------------------- | ------------------------------ | ------- | --------- | ------------ |
| host | FLAGD_HOST | string | localhost | |
| port | FLAGD_PORT | number | 8013 | |
| tls | FLAGD_TLS | boolean | false | |
| socketPath | FLAGD_SOCKET_PATH | string | - | |
| cache | FLAGD_CACHE | string | lru | lru,disabled |
| maxCacheSize | FLAGD_MAX_CACHE_SIZE | int | 1000 | |
| maxEventStreamRetries | FLAGD_MAX_EVENT_STREAM_RETRIES | int | 5 | |

### Example using TCP
### Example Using TCP

```
OpenFeature.setProvider(new FlagdProvider({
Expand All @@ -42,15 +42,15 @@ Options can be defined in the constructor or as environment variables, with cons
}))
```

### Example using a Unix socket
### Example Using a Unix Socket

```
OpenFeature.setProvider(new FlagdProvider({
socketPath: "/tmp/flagd.socks",
}))
```

### Supported events
### Supported Events

The flagd provider emits `PROVIDER_READY`, `PROVIDER_ERROR` and `PROVIDER_CONFIGURATION_CHANGED` events.

Expand All @@ -62,12 +62,18 @@ The flagd provider emits `PROVIDER_READY`, `PROVIDER_ERROR` and `PROVIDER_CONFIG

For general information on events, see the [official documentation](https://openfeature.dev/docs/reference/concepts/events).

### Flag Metadata

| Field | Type | Value |
| ------- | ------ | ------------------------------------------------- |
| `scope` | string | "selector" set for the associated source in flagd |

## Building

Run `nx package providers-flagd` to build the library.

> NOTE: [Buf](https://docs.buf.build/installation) must be installed to build locally.
## Running unit tests
## Running Unit Tests

Run `nx test providers-flagd` to execute the unit tests via [Jest](https://jestjs.io).
46 changes: 21 additions & 25 deletions libs/providers/flagd/src/lib/flagd-provider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
Client,
ErrorCode,
EvaluationContext,
FlagMetadata,
OpenFeature,
ProviderEvents,
ProviderStatus,
Expand All @@ -25,7 +26,7 @@ import {
} from '../proto/ts/schema/v1/schema';
import { EVENT_CONFIGURATION_CHANGE, EVENT_PROVIDER_READY } from './constants';
import { FlagdProvider } from './flagd-provider';
import { FlagChangeMessage, GRPCService } from './service/grpc/service';
import { FlagChangeMessage, GRPCService } from './service/grpc/grpc-service';

const REASON = StandardResolutionReasons.STATIC;
const ERROR_REASON = StandardResolutionReasons.ERROR;
Expand Down Expand Up @@ -54,6 +55,12 @@ const TEST_CONTEXT_KEY = 'context-key';
const TEST_CONTEXT_VALUE = 'context-value';
const TEST_CONTEXT = { [TEST_CONTEXT_KEY]: TEST_CONTEXT_VALUE };

const METADATA: FlagMetadata = {
metadataKey1: 'value1',
metadataKey2: 2,
metadataKey3: true,
};

describe(FlagdProvider.name, () => {
beforeEach(() => {
jest.clearAllMocks();
Expand All @@ -80,23 +87,23 @@ describe(FlagdProvider.name, () => {
value: BOOLEAN_VALUE,
variant: BOOLEAN_VARIANT,
reason: REASON,
metadata: {}
metadata: METADATA,
});
}),
resolveString: jest.fn((request: ResolveStringRequest, callback: (error: ServiceError | null, response: ResolveStringResponse) => void) => {
callback(null, {
value: STRING_VALUE,
variant: STRING_VARIANT,
reason: REASON,
metadata: {}
metadata: METADATA,
});
}),
resolveFloat: jest.fn((request: ResolveFloatRequest, callback: (error: ServiceError | null, response: ResolveFloatResponse) => void) => {
callback(null, {
value: NUMBER_VALUE,
variant: NUMBER_VARIANT,
reason: REASON,
metadata: {}
metadata: METADATA,
});
}),
resolveInt: jest.fn((): UnaryCall<ResolveIntRequest, ResolveIntResponse> => {
Expand All @@ -107,7 +114,7 @@ describe(FlagdProvider.name, () => {
value: OBJECT_VALUE,
variant: OBJECT_VARIANT,
reason: REASON,
metadata: {}
metadata: METADATA,
});
}),
} as unknown as ServiceClient;
Expand All @@ -130,6 +137,7 @@ describe(FlagdProvider.name, () => {
expect(val.value).toEqual(BOOLEAN_VALUE);
expect(val.variant).toEqual(BOOLEAN_VARIANT);
expect(val.reason).toEqual(REASON);
expect(val.flagMetadata).toEqual(METADATA);
});
});

Expand All @@ -143,6 +151,7 @@ describe(FlagdProvider.name, () => {
expect(val.value).toEqual(STRING_VALUE);
expect(val.variant).toEqual(STRING_VARIANT);
expect(val.reason).toEqual(REASON);
expect(val.flagMetadata).toEqual(METADATA);
});
});

Expand All @@ -156,6 +165,7 @@ describe(FlagdProvider.name, () => {
expect(val.value).toEqual(NUMBER_VALUE);
expect(val.variant).toEqual(NUMBER_VARIANT);
expect(val.reason).toEqual(REASON);
expect(val.flagMetadata).toEqual(METADATA);
});
});

Expand All @@ -169,6 +179,7 @@ describe(FlagdProvider.name, () => {
expect(val.value).toEqual({ [OBJECT_INNER_KEY]: OBJECT_INNER_VALUE });
expect(val.variant).toEqual(OBJECT_VARIANT);
expect(val.reason).toEqual(REASON);
expect(val.flagMetadata).toEqual(METADATA);
});
});

Expand All @@ -184,6 +195,7 @@ describe(FlagdProvider.name, () => {
expect(val.value).toEqual(BOOLEAN_VALUE);
expect(val.variant).toEqual(BOOLEAN_VARIANT);
expect(val.reason).toEqual(REASON);
expect(val.flagMetadata).toEqual(METADATA);
});
});
});
Expand All @@ -206,22 +218,16 @@ describe(FlagdProvider.name, () => {
resolveBoolean: jest.fn((request: ResolveBooleanRequest, callback: (error: ServiceError | null, response: ResolveBooleanResponse) => void) => {
callback(null, {
variant: BOOLEAN_VARIANT,
reason: REASON,
metadata: {}
} as ResolveBooleanResponse);
}),
resolveString: jest.fn((request: ResolveStringRequest, callback: (error: ServiceError | null, response: ResolveStringResponse) => void) => {
callback(null, {
variant: STRING_VARIANT,
reason: REASON,
metadata: {}
} as ResolveStringResponse);
}),
resolveFloat: jest.fn((request: ResolveFloatRequest, callback: (error: ServiceError | null, response: ResolveFloatResponse) => void) => {
callback(null, {
variant: NUMBER_VARIANT,
reason: REASON,
metadata: {}
} as ResolveFloatResponse);
}),
resolveInt: jest.fn((): UnaryCall<ResolveIntRequest, ResolveIntResponse> => {
Expand All @@ -230,8 +236,6 @@ describe(FlagdProvider.name, () => {
resolveObject: jest.fn((request: ResolveObjectRequest, callback: (error: ServiceError | null, response: ResolveObjectResponse) => void) => {
callback(null, {
variant: OBJECT_VARIANT,
reason: REASON,
metadata: {}
} as ResolveObjectResponse);
}),
} as unknown as ServiceClient;
Expand All @@ -245,54 +249,46 @@ describe(FlagdProvider.name, () => {
});

describe(FlagdProvider.prototype.resolveBooleanEvaluation.name, () => {
it(`should call ${ServiceClient.prototype.resolveBoolean} with key and context and return details`, async () => {
it(`should call ${ServiceClient.prototype.resolveBoolean} with key and context and return falsy value`, async () => {
const val = await client.getBooleanDetails(BOOLEAN_KEY, false, TEST_CONTEXT);
expect(basicServiceClientMock.resolveBoolean).toHaveBeenCalledWith({
flagKey: BOOLEAN_KEY,
context: TEST_CONTEXT,
}, expect.any(Function));
expect(val.value).toEqual(false);
expect(val.variant).toEqual(BOOLEAN_VARIANT);
expect(val.reason).toEqual(REASON);
});
});

describe(FlagdProvider.prototype.resolveStringEvaluation.name, () => {
it(`should call ${ServiceClient.prototype.resolveString} with key and context and return details`, async () => {
it(`should call ${ServiceClient.prototype.resolveString} with key and context and return falsy value`, async () => {
const val = await client.getStringDetails(STRING_KEY, '', TEST_CONTEXT);
expect(basicServiceClientMock.resolveString).toHaveBeenCalledWith({
flagKey: STRING_KEY,
context: TEST_CONTEXT,
}, expect.any(Function));
expect(val.value).toEqual('');
expect(val.variant).toEqual(STRING_VARIANT);
expect(val.reason).toEqual(REASON);
});
});

describe(FlagdProvider.prototype.resolveNumberEvaluation.name, () => {
it(`should call ${ServiceClient.prototype.resolveFloat} with key and context and return details`, async () => {
it(`should call ${ServiceClient.prototype.resolveFloat} with key and context and return falsy value`, async () => {
const val = await client.getNumberDetails(NUMBER_KEY, 0, TEST_CONTEXT);
expect(basicServiceClientMock.resolveFloat).toHaveBeenCalledWith({
flagKey: NUMBER_KEY,
context: TEST_CONTEXT,
}, expect.any(Function));
expect(val.value).toEqual(0);
expect(val.variant).toEqual(NUMBER_VARIANT);
expect(val.reason).toEqual(REASON);
});
});

describe(FlagdProvider.prototype.resolveObjectEvaluation.name, () => {
it(`should call ${ServiceClient.prototype.resolveObject} with key and context and return details`, async () => {
it(`should call ${ServiceClient.prototype.resolveObject} with key and context and return falsy value`, async () => {
const val = await client.getObjectDetails(OBJECT_KEY, {}, TEST_CONTEXT);
expect(basicServiceClientMock.resolveObject).toHaveBeenCalledWith({
flagKey: OBJECT_KEY,
context: TEST_CONTEXT,
}, expect.any(Function));
expect(val.value).toEqual({});
expect(val.variant).toEqual(OBJECT_VARIANT);
expect(val.reason).toEqual(REASON);
});
});
});
Expand Down
2 changes: 1 addition & 1 deletion libs/providers/flagd/src/lib/flagd-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
ResolutionDetails,
} from '@openfeature/js-sdk';
import { FlagdProviderOptions, getConfig } from './configuration';
import { GRPCService } from './service/grpc/service';
import { GRPCService } from './service/grpc/grpc-service';
import { Service } from './service/service';

export class FlagdProvider implements Provider {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -280,11 +280,12 @@ export class GRPCService implements Service {
.call(this._client, { flagKey, context })
.then((resolved) => resolved, this.onRejected);

const resolved = {
const resolved: ResolutionDetails<T> = {
// invoke the parser method if passed
value: parser.call(this, response.value as T),
reason: response.reason,
variant: response.variant,
flagMetadata: response.metadata,
};

logger.debug(
Expand Down

0 comments on commit c8a80c6

Please sign in to comment.