diff --git a/specification.json b/specification.json index 14e4cf6d..341e4200 100644 --- a/specification.json +++ b/specification.json @@ -539,7 +539,7 @@ { "id": "Requirement 3.2.3", "machine_id": "requirement_3_2_3", - "content": "Evaluation context MUST be merged in the order: API (global; lowest precedence) - client - invocation - before hooks (highest precedence), with duplicate values being overwritten.", + "content": "Evaluation context MUST be merged in the order: API (global; lowest precedence) - transaction - client - invocation - before hooks (highest precedence), with duplicate values being overwritten.", "RFC 2119 keyword": "MUST", "children": [] }, @@ -565,6 +565,65 @@ } ] }, + { + "id": "Condition 3.3.1", + "machine_id": "condition_3_3_1", + "content": "The implementation uses the dynamic-context paradigm.", + "RFC 2119 keyword": null, + "children": [ + { + "id": "Conditional Requirement 3.3.1.1", + "machine_id": "conditional_requirement_3_3_1_1", + "content": "The API SHOULD have a method for setting a `transaction context propagator`.", + "RFC 2119 keyword": "SHOULD", + "children": [] + } + ] + }, + { + "id": "Condition 3.3.1.2", + "machine_id": "condition_3_3_1_2", + "content": "The SDK implements context propagation.", + "RFC 2119 keyword": null, + "children": [ + { + "id": "Conditional Requirement 3.3.1.2.1", + "machine_id": "conditional_requirement_3_3_1_2_1", + "content": "The API MUST have a method for setting the `evaluation context` of the `transaction context propagator` for the current transaction.", + "RFC 2119 keyword": "MUST", + "children": [] + }, + { + "id": "Conditional Requirement 3.3.1.2.2", + "machine_id": "conditional_requirement_3_3_1_2_2", + "content": "A `transaction context propagator` MUST have a method for setting the `evaluation context` of the current transaction.", + "RFC 2119 keyword": "MUST", + "children": [] + }, + { + "id": "Conditional Requirement 3.3.1.2.3", + "machine_id": "conditional_requirement_3_3_1_2_3", + "content": "A `transaction context propagator` MUST have a method for getting the `evaluation context` of the current transaction.", + "RFC 2119 keyword": "MUST", + "children": [] + } + ] + }, + { + "id": "Condition 3.3.2", + "machine_id": "condition_3_3_2", + "content": "The implementation uses the static-context paradigm.", + "RFC 2119 keyword": null, + "children": [ + { + "id": "Conditional Requirement 3.3.2.1", + "machine_id": "conditional_requirement_3_3_2_1", + "content": "The API MUST NOT have a method for setting a `transaction context propagator`.", + "RFC 2119 keyword": "MUST NOT", + "children": [] + } + ] + }, { "id": "Requirement 4.1.1", "machine_id": "requirement_4_1_1", diff --git a/specification/glossary.md b/specification/glossary.md index 7ac65701..5f759fae 100644 --- a/specification/glossary.md +++ b/specification/glossary.md @@ -31,6 +31,7 @@ This document defines some terms that are used across this specification. - [Domain](#domain) - [Integration](#integration) - [Evaluation Context](#evaluation-context) + - [Transaction Context Propagator](#transaction-context-propagator) - [Evaluating Flag Values](#evaluating-flag-values) - [Resolving Flag Values](#resolving-flag-values) - [Flagging specifics](#flagging-specifics) @@ -120,6 +121,10 @@ An SDK-compliant secondary function that is abstracted by the Feature Flag API, Context object for flag evaluation, which may contain information about the runtime environment, details of the transport method encapsulating the flag evaluation, the host, the client, the subject (user), etc. This data may be used as a basis for differential evaluation of feature flags based on rules that can be defined in the flag system. Context data may be provided by merging static global context, arguments to flag evaluation, and implicit language-dependant state propagation mechanisms (thread-local storage, promise chains, continuations, etc). +### Transaction Context Propagator + +An SDK-compliant implementation that stores and returns transaction-specific evaluation context. A _transaction_ might be a web request or application event, which carries its contextual data in a thread or continuation storage. + ### Evaluating Flag Values The process of retrieving a feature flag value in it's entirety, including: diff --git a/specification/sections/03-evaluation-context.md b/specification/sections/03-evaluation-context.md index 7d3222fe..9a7641c4 100644 --- a/specification/sections/03-evaluation-context.md +++ b/specification/sections/03-evaluation-context.md @@ -95,21 +95,25 @@ See [setting a provider](./01-flag-evaluation.md#setting-a-provider), [domain](. #### Requirement 3.2.3 -> Evaluation context **MUST** be merged in the order: API (global; lowest precedence) -> client -> invocation -> before hooks (highest precedence), with duplicate values being overwritten. +> Evaluation context **MUST** be merged in the order: API (global; lowest precedence) -> transaction -> client -> invocation -> before hooks (highest precedence), with duplicate values being overwritten. -Any fields defined in the client `evaluation context` will overwrite duplicate fields defined globally, and fields defined in the invocation `evaluation context` will overwrite duplicate fields defined globally or on the client. Any resulting `evaluation context` from a [before hook](./04-hooks.md#requirement-434) will overwrite duplicate fields defined globally, on the client, or in the invocation. +Any fields defined in the transaction `evaluation context` will overwrite duplicate fields defined in the global `evaluation context`, any fields defined in the client `evaluation context` will overwrite duplicate fields defined in the transaction `evaluation context`, and fields defined in the invocation `evaluation context` will overwrite duplicate fields defined globally or on the client. Any resulting `evaluation context` from a [before hook](./04-hooks.md#requirement-434) will overwrite duplicate fields defined globally, on the client, or in the invocation. ```mermaid flowchart LR - global("API (global)") - client("Client") - invocation("Invocation") - hook("Before Hooks") - global --> client - client --> invocation - invocation --> hook + global("API (global)") + transaction("Transaction") + client("Client") + invocation("Invocation") + hook("Before Hooks") + global --> transaction + transaction --> client + client --> invocation + invocation --> hook ``` +This describes the precedence of all `evaluation context` variants. Depending on the `paradigm`, not all variants might be available in an `SDK` implementation. + #### Condition 3.2.4 [![experimental](https://img.shields.io/static/v1?label=Status&message=experimental&color=orange)](https://github.com/open-feature/spec/tree/main/specification#experimental) @@ -128,4 +132,88 @@ The SDK implementation must run the `on context changed` handler on all register > When the `evaluation context` for a specific provider is set, the `on context changed` handler **MUST** only run on the associated provider. -The SDK implementation must run the `on context changed` handler only on the provider that is scoped to the mutated `evaluation context`. \ No newline at end of file +The SDK implementation must run the `on context changed` handler only on the provider that is scoped to the mutated `evaluation context`. + +### 3.3 Context Propagation + +[![experimental](https://img.shields.io/static/v1?label=Status&message=experimental&color=orange)](https://github.com/open-feature/spec/tree/main/specification#experimental) + +`Transaction context` is a container for transaction-specific `evaluation context` (e.g. user id, user agent, IP). +Transaction context can be set where specific data is available (e.g. an auth service or request handler) and by using the `transaction context propagator` it will automatically be applied to all flag evaluations within a transaction (e.g. a request or thread). + +The following shows a possible TypeScript implementation using [AsyncLocalStorage (async_hooks)](https://nodejs.org/api/async_context.html): + +```typescript +export class AsyncLocalStorageTransactionContext implements TransactionContextPropagator { + private asyncLocalStorage = new AsyncLocalStorage(); + + getTransactionContext(): EvaluationContext { + return this.asyncLocalStorage.getStore() ?? {}; + } + setTransactionContext(context: EvaluationContext, callback: () => void): void { + this.asyncLocalStorage.run(context, callback); + } +} + +/** + * This example is based on an express middleware. + */ +app.use((req: Request, res: Response, next: NextFunction) => { + const ip = res.headers.get("X-Forwarded-For") + OpenFeature.setTransactionContext({ targetingKey: req.user.id, ipAddress: ip }, () => { + // The transaction context is used in any flag evaluation throughout the whole call chain of next + next(); + }); +}) +``` + +#### Condition 3.3.1 + +> The implementation uses the dynamic-context paradigm. + +see: [dynamic-context paradigm](../glossary.md#dynamic-context-paradigm) + +##### Conditional Requirement 3.3.1.1 + +> The API **SHOULD** have a method for setting a `transaction context propagator`. + +If there already is a `transaction context propagator`, it is replaced with the new one. + +#### Condition 3.3.1.2 + +> The SDK implements context propagation. + +A language may not have any applicable way of implementing `transaction context propagation` so the language SDK might not implement context propagation. + +##### Conditional Requirement 3.3.1.2.1 + +> The API **MUST** have a method for setting the `evaluation context` of the `transaction context propagator` for the current transaction. + +If a `transaction context propagator` is set, the SDK will call the [method defined in 3.3.1.3](#conditional-requirement-33122) with this `evaluation context` and so this `evaluation context` will be available during the current transaction. +If no `transaction context propagator` is set, this `evaluation context` is not used for evaluations. +This method then can be used for example in a request handler to add request-specific information to the `evaluation context`. + +##### Conditional Requirement 3.3.1.2.2 + +> A `transaction context propagator` **MUST** have a method for setting the `evaluation context` of the current transaction. + +A `transaction context propagator` is responsible for persisting context for the duration of a single transaction. +Typically, a transaction context propagator will propagate the context using a language-specific carrier such as [ThreadLocal (Java)](https://docs.oracle.com/javase/8/docs/api/java/lang/ThreadLocal.html), [async hooks (Node.js)](https://nodejs.org/api/async_hooks.html), [Context (Go)](https://pkg.go.dev/context) or another similar mechanism. + +##### Conditional Requirement 3.3.1.2.3 + +> A `transaction context propagator` **MUST** have a method for getting the `evaluation context` of the current transaction. + +This will be used by the SDK implementation when merging the context for evaluating a feature flag. + +#### Condition 3.3.2 + +> The implementation uses the static-context paradigm. + +see: [static-context paradigm](../glossary.md#static-context-paradigm) + +##### Conditional Requirement 3.3.2.1 + +> The API **MUST NOT** have a method for setting a `transaction context propagator`. + +In the static-context paradigm, context is global, so there must not be different contexts between transactions.