Skip to content

Commit 1d251ff

Browse files
beeme1mrtoddbaert
andauthored
feat: add transaction propagation (#212)
Signed-off-by: Michael Beemer <[email protected]> Co-authored-by: Todd Baert <[email protected]>
1 parent 31ef356 commit 1d251ff

File tree

7 files changed

+242
-48
lines changed

7 files changed

+242
-48
lines changed

.eslintrc.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@
2626
],
2727
"quotes":[
2828
"error",
29-
"single"
29+
"single",
30+
{
31+
"avoidEscape": true
32+
}
3033
],
3134
"semi":[
3235
"error",

src/client.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ export class OpenFeatureClient implements Client {
194194
// merge global and client contexts
195195
const mergedContext = {
196196
...OpenFeature.getContext(),
197+
...OpenFeature.getTransactionContext(),
197198
...this._context,
198199
...invocationContext,
199200
};
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { EvaluationContext, TransactionContextPropagator } from './types';
2+
3+
class NoopTransactionContextPropagator implements TransactionContextPropagator {
4+
getTransactionContext(): EvaluationContext {
5+
return {};
6+
}
7+
8+
setTransactionContext(_: EvaluationContext, callback: () => void): void {
9+
callback();
10+
}
11+
}
12+
13+
export const NOOP_TRANSACTION_CONTEXT_PROPAGATOR = new NoopTransactionContextPropagator();

src/open-feature.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,19 @@
11
import { OpenFeatureClient } from './client';
22
import { DefaultLogger, SafeLogger } from './logger';
33
import { NOOP_PROVIDER } from './no-op-provider';
4-
import { Client, EvaluationContext, FlagValue, GlobalApi, Hook, Logger, Provider, ProviderMetadata } from './types';
4+
import { NOOP_TRANSACTION_CONTEXT_PROPAGATOR } from './no-op-transaction-context-propagator';
5+
import {
6+
Client,
7+
EvaluationContext,
8+
FlagValue,
9+
GlobalApi,
10+
Hook,
11+
Logger,
12+
Provider,
13+
ProviderMetadata,
14+
TransactionContext,
15+
TransactionContextPropagator,
16+
} from './types';
517

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

1426
class OpenFeatureAPI implements GlobalApi {
1527
private _provider: Provider = NOOP_PROVIDER;
28+
private _transactionContextPropagator: TransactionContextPropagator = NOOP_TRANSACTION_CONTEXT_PROPAGATOR;
1629
private _context: EvaluationContext = {};
1730
private _hooks: Hook[] = [];
1831
private _logger: Logger = new DefaultLogger();
@@ -78,6 +91,37 @@ class OpenFeatureAPI implements GlobalApi {
7891
getContext(): EvaluationContext {
7992
return this._context;
8093
}
94+
95+
setTransactionContextPropagator(transactionContextPropagator: TransactionContextPropagator): OpenFeatureAPI {
96+
const baseMessage = 'Invalid TransactionContextPropagator, will not be set: ';
97+
if (typeof transactionContextPropagator?.getTransactionContext !== 'function') {
98+
this._logger.error(`${baseMessage}: getTransactionContext is not a function.`);
99+
} else if (typeof transactionContextPropagator?.setTransactionContext !== 'function') {
100+
this._logger.error(`${baseMessage}: setTransactionContext is not a function.`);
101+
} else {
102+
this._transactionContextPropagator = transactionContextPropagator;
103+
}
104+
return this;
105+
}
106+
107+
setTransactionContext<R>(
108+
transactionContext: TransactionContext,
109+
callback: (...args: unknown[]) => R,
110+
...args: unknown[]
111+
): void {
112+
this._transactionContextPropagator.setTransactionContext(transactionContext, callback, ...args);
113+
}
114+
115+
getTransactionContext(): TransactionContext {
116+
try {
117+
return this._transactionContextPropagator.getTransactionContext();
118+
} catch (err: unknown) {
119+
const error = err as Error | undefined;
120+
this._logger.error(`Error getting transaction context: ${error?.message}, returning empty context.`);
121+
this._logger.error(error?.stack);
122+
return {};
123+
}
124+
}
81125
}
82126

83127
export const OpenFeature = OpenFeatureAPI.getInstance();

src/types.ts

Lines changed: 71 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
type PrimitiveValue = null | boolean | string | number ;
1+
type PrimitiveValue = null | boolean | string | number;
22

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

@@ -12,7 +12,11 @@ export type JsonValue = PrimitiveValue | JsonObject | JsonArray;
1212
/**
1313
* Represents a JSON node value, or Date.
1414
*/
15-
export type EvaluationContextValue = PrimitiveValue | Date | { [key: string]: EvaluationContextValue } | EvaluationContextValue[];
15+
export type EvaluationContextValue =
16+
| PrimitiveValue
17+
| Date
18+
| { [key: string]: EvaluationContextValue }
19+
| EvaluationContextValue[];
1620

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

4549
export interface Features {
46-
4750
/**
4851
* Performs a flag evaluation that returns a boolean.
4952
*
@@ -86,7 +89,7 @@ export interface Features {
8689
* @param {FlagEvaluationOptions} options Additional flag evaluation options
8790
* @returns {Promise<T>} Flag evaluation response
8891
*/
89-
getStringValue(
92+
getStringValue(
9093
flagKey: string,
9194
defaultValue: string,
9295
context?: EvaluationContext,
@@ -155,7 +158,7 @@ export interface Features {
155158
* @param {FlagEvaluationOptions} options Additional flag evaluation options
156159
* @returns {Promise<EvaluationDetails<T>>} Flag evaluation details response
157160
*/
158-
getNumberDetails(
161+
getNumberDetails(
159162
flagKey: string,
160163
defaultValue: number,
161164
context?: EvaluationContext,
@@ -277,7 +280,7 @@ export const StandardResolutionReasons = {
277280
* The resolved value was the result of a dynamic evaluation, such as a rule or specific user-targeting.
278281
*/
279282
TARGETING_MATCH: 'TARGETING_MATCH',
280-
283+
281284
/**
282285
* The resolved value was the result of pseudorandom assignment.
283286
*/
@@ -301,7 +304,7 @@ export const StandardResolutionReasons = {
301304
/**
302305
* The resolved value was the result of an error.
303306
*
304-
* Note: The `errorCode` and `errorMessage` fields may contain additional details of this error.
307+
* Note: The `errorCode` and `errorMessage` fields may contain additional details of this error.
305308
*/
306309
ERROR: 'ERROR',
307310
} as const;
@@ -361,7 +364,11 @@ export interface Client extends EvaluationLifeCycle<Client>, Features, ManageCon
361364
readonly metadata: ClientMetadata;
362365
}
363366

364-
export interface GlobalApi extends EvaluationLifeCycle<GlobalApi>, ManageContext<GlobalApi>, ManageLogger<GlobalApi> {
367+
export interface GlobalApi
368+
extends EvaluationLifeCycle<GlobalApi>,
369+
ManageContext<GlobalApi>,
370+
ManageLogger<GlobalApi>,
371+
ManageTransactionContextPropagator<GlobalApi> {
365372
readonly providerMetadata: ProviderMetadata;
366373
/**
367374
* A factory function for creating new OpenFeature clients. Clients can contain
@@ -374,15 +381,15 @@ export interface GlobalApi extends EvaluationLifeCycle<GlobalApi>, ManageContext
374381
* @returns {Client} OpenFeature Client
375382
*/
376383
getClient(name?: string, version?: string, context?: EvaluationContext): Client;
377-
384+
378385
/**
379386
* Sets the provider that OpenFeature will use for flag evaluations. Setting
380387
* a provider supersedes the current provider used in new and existing clients.
381388
*
382389
* @param {Provider} provider The provider responsible for flag evaluations.
383390
* @returns {GlobalApi} OpenFeature API
384391
*/
385-
setProvider(provider: Provider): GlobalApi
392+
setProvider(provider: Provider): GlobalApi;
386393
}
387394

388395
interface EvaluationLifeCycle<T> {
@@ -442,7 +449,7 @@ interface ManageLogger<T> {
442449
* unless overridden in a particular client.
443450
*
444451
* @template T The type of the receiver
445-
* @param {Logger} logger The logger to to be used
452+
* @param {Logger} logger The logger to be used
446453
* @returns {T} The receiver (this object)
447454
*/
448455
setLogger(logger: Logger): T;
@@ -520,3 +527,56 @@ export interface Hook<T extends FlagValue = FlagValue> {
520527
*/
521528
finally?(hookContext: Readonly<HookContext<T>>, hookHints?: HookHints): Promise<void> | void;
522529
}
530+
531+
/**
532+
* Transaction context is a mechanism for adding transaction specific context that
533+
* is merged with evaluation context prior to flag evaluation. Examples of potential
534+
* transaction specific context include: a user id, user agent, or request path.
535+
*/
536+
export type TransactionContext = EvaluationContext;
537+
538+
interface ManageTransactionContextPropagator<T> extends TransactionContextPropagator {
539+
/**
540+
* EXPERIMENTAL: Transaction context propagation is experimental and subject to change.
541+
* The OpenFeature Enhancement Proposal regarding transaction context can be found [here](https://github.com/open-feature/ofep/pull/32).
542+
*
543+
* Sets a transaction context propagator on this receiver. The transaction context
544+
* propagator is responsible for persisting context for the duration of a single
545+
* transaction.
546+
*
547+
* @template T The type of the receiver
548+
* @param {TransactionContextPropagator} transactionContextPropagator The context propagator to be used
549+
* @returns {T} The receiver (this object)
550+
*/
551+
setTransactionContextPropagator(transactionContextPropagator: TransactionContextPropagator): T;
552+
}
553+
554+
export interface TransactionContextPropagator {
555+
/**
556+
* EXPERIMENTAL: Transaction context propagation is experimental and subject to change.
557+
* The OpenFeature Enhancement Proposal regarding transaction context can be found [here](https://github.com/open-feature/ofep/pull/32).
558+
*
559+
* Returns the currently defined transaction context using the registered transaction
560+
* context propagator.
561+
*
562+
* @returns {TransactionContext} The current transaction context
563+
*/
564+
getTransactionContext(): TransactionContext;
565+
566+
/**
567+
* EXPERIMENTAL: Transaction context propagation is experimental and subject to change.
568+
* The OpenFeature Enhancement Proposal regarding transaction context can be found [here](https://github.com/open-feature/ofep/pull/32).
569+
*
570+
* Sets the transaction context using the registered transaction context propagator.
571+
*
572+
* @template R The return value of the callback
573+
* @param {TransactionContext} transactionContext The transaction specific context
574+
* @param {(...args: unknown[]) => R} callback Callback function used to set the transaction context on the stack
575+
* @param {...unknown[]} args Optional arguments that are passed to the callback function
576+
*/
577+
setTransactionContext<R>(
578+
transactionContext: TransactionContext,
579+
callback: (...args: unknown[]) => R,
580+
...args: unknown[]
581+
): void;
582+
}

0 commit comments

Comments
 (0)