Skip to content

Commit

Permalink
feat: add transaction propagation (open-feature#212)
Browse files Browse the repository at this point in the history
Signed-off-by: Michael Beemer <[email protected]>
Co-authored-by: Todd Baert <[email protected]>
  • Loading branch information
beeme1mr and toddbaert authored Oct 3, 2022
1 parent 31ef356 commit 1d251ff
Show file tree
Hide file tree
Showing 7 changed files with 242 additions and 48 deletions.
5 changes: 4 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@
],
"quotes":[
"error",
"single"
"single",
{
"avoidEscape": true
}
],
"semi":[
"error",
Expand Down
1 change: 1 addition & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ export class OpenFeatureClient implements Client {
// merge global and client contexts
const mergedContext = {
...OpenFeature.getContext(),
...OpenFeature.getTransactionContext(),
...this._context,
...invocationContext,
};
Expand Down
13 changes: 13 additions & 0 deletions src/no-op-transaction-context-propagator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { EvaluationContext, TransactionContextPropagator } from './types';

class NoopTransactionContextPropagator implements TransactionContextPropagator {
getTransactionContext(): EvaluationContext {
return {};
}

setTransactionContext(_: EvaluationContext, callback: () => void): void {
callback();
}
}

export const NOOP_TRANSACTION_CONTEXT_PROPAGATOR = new NoopTransactionContextPropagator();
46 changes: 45 additions & 1 deletion src/open-feature.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
import { OpenFeatureClient } from './client';
import { DefaultLogger, SafeLogger } from './logger';
import { NOOP_PROVIDER } from './no-op-provider';
import { Client, EvaluationContext, FlagValue, GlobalApi, Hook, Logger, Provider, ProviderMetadata } from './types';
import { NOOP_TRANSACTION_CONTEXT_PROPAGATOR } from './no-op-transaction-context-propagator';
import {
Client,
EvaluationContext,
FlagValue,
GlobalApi,
Hook,
Logger,
Provider,
ProviderMetadata,
TransactionContext,
TransactionContextPropagator,
} from './types';

// use a symbol as a key for the global singleton
const GLOBAL_OPENFEATURE_API_KEY = Symbol.for('@openfeature/js.api');
Expand All @@ -13,6 +25,7 @@ const _globalThis = globalThis as OpenFeatureGlobal;

class OpenFeatureAPI implements GlobalApi {
private _provider: Provider = NOOP_PROVIDER;
private _transactionContextPropagator: TransactionContextPropagator = NOOP_TRANSACTION_CONTEXT_PROPAGATOR;
private _context: EvaluationContext = {};
private _hooks: Hook[] = [];
private _logger: Logger = new DefaultLogger();
Expand Down Expand Up @@ -78,6 +91,37 @@ class OpenFeatureAPI implements GlobalApi {
getContext(): EvaluationContext {
return this._context;
}

setTransactionContextPropagator(transactionContextPropagator: TransactionContextPropagator): OpenFeatureAPI {
const baseMessage = 'Invalid TransactionContextPropagator, will not be set: ';
if (typeof transactionContextPropagator?.getTransactionContext !== 'function') {
this._logger.error(`${baseMessage}: getTransactionContext is not a function.`);
} else if (typeof transactionContextPropagator?.setTransactionContext !== 'function') {
this._logger.error(`${baseMessage}: setTransactionContext is not a function.`);
} else {
this._transactionContextPropagator = transactionContextPropagator;
}
return this;
}

setTransactionContext<R>(
transactionContext: TransactionContext,
callback: (...args: unknown[]) => R,
...args: unknown[]
): void {
this._transactionContextPropagator.setTransactionContext(transactionContext, callback, ...args);
}

getTransactionContext(): TransactionContext {
try {
return this._transactionContextPropagator.getTransactionContext();
} catch (err: unknown) {
const error = err as Error | undefined;
this._logger.error(`Error getting transaction context: ${error?.message}, returning empty context.`);
this._logger.error(error?.stack);
return {};
}
}
}

export const OpenFeature = OpenFeatureAPI.getInstance();
82 changes: 71 additions & 11 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
type PrimitiveValue = null | boolean | string | number ;
type PrimitiveValue = null | boolean | string | number;

export type JsonObject = { [key: string]: JsonValue };

Expand All @@ -12,7 +12,11 @@ export type JsonValue = PrimitiveValue | JsonObject | JsonArray;
/**
* Represents a JSON node value, or Date.
*/
export type EvaluationContextValue = PrimitiveValue | Date | { [key: string]: EvaluationContextValue } | EvaluationContextValue[];
export type EvaluationContextValue =
| PrimitiveValue
| Date
| { [key: string]: EvaluationContextValue }
| EvaluationContextValue[];

/**
* A container for arbitrary contextual data that can be used as a basis for dynamic evaluation
Expand Down Expand Up @@ -43,7 +47,6 @@ export interface Logger {
}

export interface Features {

/**
* Performs a flag evaluation that returns a boolean.
*
Expand Down Expand Up @@ -86,7 +89,7 @@ export interface Features {
* @param {FlagEvaluationOptions} options Additional flag evaluation options
* @returns {Promise<T>} Flag evaluation response
*/
getStringValue(
getStringValue(
flagKey: string,
defaultValue: string,
context?: EvaluationContext,
Expand Down Expand Up @@ -155,7 +158,7 @@ export interface Features {
* @param {FlagEvaluationOptions} options Additional flag evaluation options
* @returns {Promise<EvaluationDetails<T>>} Flag evaluation details response
*/
getNumberDetails(
getNumberDetails(
flagKey: string,
defaultValue: number,
context?: EvaluationContext,
Expand Down Expand Up @@ -277,7 +280,7 @@ export const StandardResolutionReasons = {
* The resolved value was the result of a dynamic evaluation, such as a rule or specific user-targeting.
*/
TARGETING_MATCH: 'TARGETING_MATCH',

/**
* The resolved value was the result of pseudorandom assignment.
*/
Expand All @@ -301,7 +304,7 @@ export const StandardResolutionReasons = {
/**
* The resolved value was the result of an error.
*
* Note: The `errorCode` and `errorMessage` fields may contain additional details of this error.
* Note: The `errorCode` and `errorMessage` fields may contain additional details of this error.
*/
ERROR: 'ERROR',
} as const;
Expand Down Expand Up @@ -361,7 +364,11 @@ export interface Client extends EvaluationLifeCycle<Client>, Features, ManageCon
readonly metadata: ClientMetadata;
}

export interface GlobalApi extends EvaluationLifeCycle<GlobalApi>, ManageContext<GlobalApi>, ManageLogger<GlobalApi> {
export interface GlobalApi
extends EvaluationLifeCycle<GlobalApi>,
ManageContext<GlobalApi>,
ManageLogger<GlobalApi>,
ManageTransactionContextPropagator<GlobalApi> {
readonly providerMetadata: ProviderMetadata;
/**
* A factory function for creating new OpenFeature clients. Clients can contain
Expand All @@ -374,15 +381,15 @@ export interface GlobalApi extends EvaluationLifeCycle<GlobalApi>, ManageContext
* @returns {Client} OpenFeature Client
*/
getClient(name?: string, version?: string, context?: EvaluationContext): Client;

/**
* Sets the provider that OpenFeature will use for flag evaluations. Setting
* a provider supersedes the current provider used in new and existing clients.
*
* @param {Provider} provider The provider responsible for flag evaluations.
* @returns {GlobalApi} OpenFeature API
*/
setProvider(provider: Provider): GlobalApi
setProvider(provider: Provider): GlobalApi;
}

interface EvaluationLifeCycle<T> {
Expand Down Expand Up @@ -442,7 +449,7 @@ interface ManageLogger<T> {
* unless overridden in a particular client.
*
* @template T The type of the receiver
* @param {Logger} logger The logger to to be used
* @param {Logger} logger The logger to be used
* @returns {T} The receiver (this object)
*/
setLogger(logger: Logger): T;
Expand Down Expand Up @@ -520,3 +527,56 @@ export interface Hook<T extends FlagValue = FlagValue> {
*/
finally?(hookContext: Readonly<HookContext<T>>, hookHints?: HookHints): Promise<void> | void;
}

/**
* Transaction context is a mechanism for adding transaction specific context that
* is merged with evaluation context prior to flag evaluation. Examples of potential
* transaction specific context include: a user id, user agent, or request path.
*/
export type TransactionContext = EvaluationContext;

interface ManageTransactionContextPropagator<T> extends TransactionContextPropagator {
/**
* EXPERIMENTAL: Transaction context propagation is experimental and subject to change.
* The OpenFeature Enhancement Proposal regarding transaction context can be found [here](https://github.com/open-feature/ofep/pull/32).
*
* Sets a transaction context propagator on this receiver. The transaction context
* propagator is responsible for persisting context for the duration of a single
* transaction.
*
* @template T The type of the receiver
* @param {TransactionContextPropagator} transactionContextPropagator The context propagator to be used
* @returns {T} The receiver (this object)
*/
setTransactionContextPropagator(transactionContextPropagator: TransactionContextPropagator): T;
}

export interface TransactionContextPropagator {
/**
* EXPERIMENTAL: Transaction context propagation is experimental and subject to change.
* The OpenFeature Enhancement Proposal regarding transaction context can be found [here](https://github.com/open-feature/ofep/pull/32).
*
* Returns the currently defined transaction context using the registered transaction
* context propagator.
*
* @returns {TransactionContext} The current transaction context
*/
getTransactionContext(): TransactionContext;

/**
* EXPERIMENTAL: Transaction context propagation is experimental and subject to change.
* The OpenFeature Enhancement Proposal regarding transaction context can be found [here](https://github.com/open-feature/ofep/pull/32).
*
* Sets the transaction context using the registered transaction context propagator.
*
* @template R The return value of the callback
* @param {TransactionContext} transactionContext The transaction specific context
* @param {(...args: unknown[]) => R} callback Callback function used to set the transaction context on the stack
* @param {...unknown[]} args Optional arguments that are passed to the callback function
*/
setTransactionContext<R>(
transactionContext: TransactionContext,
callback: (...args: unknown[]) => R,
...args: unknown[]
): void;
}
Loading

0 comments on commit 1d251ff

Please sign in to comment.