diff --git a/specification.json b/specification.json index 652c2c98..39b3a858 100644 --- a/specification.json +++ b/specification.json @@ -678,7 +678,7 @@ { "id": "Requirement 4.1.1", "machine_id": "requirement_4_1_1", - "content": "Hook context MUST provide: the `flag key`, `flag value type`, `evaluation context`, and the `default value`.", + "content": "Hook context MUST provide: the `flag key`, `flag value type`, `evaluation context`, `default value`, and `hook data`.", "RFC 2119 keyword": "MUST", "children": [] }, @@ -711,6 +711,13 @@ } ] }, + { + "id": "Requirement 4.1.5", + "machine_id": "requirement_4_1_5", + "content": "The `hook data` MUST be mutable.", + "RFC 2119 keyword": "MUST", + "children": [] + }, { "id": "Requirement 4.2.1", "machine_id": "requirement_4_2_1", @@ -754,6 +761,13 @@ "RFC 2119 keyword": "MUST", "children": [] }, + { + "id": "Requirement 4.3.2", + "machine_id": "requirement_4_3_2", + "content": "`Hook data` MUST must be created before the first `stage` invoked in a hook for a specific evaluation and propagated between each `stage` of the hook. The hook data is not shared between different hooks.", + "RFC 2119 keyword": "MUST", + "children": [] + }, { "id": "Condition 4.3.2", "machine_id": "condition_4_3_2", @@ -904,6 +918,13 @@ "RFC 2119 keyword": "MUST NOT", "children": [] }, + { + "id": "Requirement 4.6.1", + "machine_id": "requirement_4_6_1", + "content": "`hook data` MUST be a structure supports definition of arbitrary properties, with keys of type `string`, and values of type `boolean | string | number | datetime | structure`.", + "RFC 2119 keyword": "MUST", + "children": [] + }, { "id": "Requirement 5.1.1", "machine_id": "requirement_5_1_1", diff --git a/specification/sections/04-hooks.md b/specification/sections/04-hooks.md index af7bbaa6..310294bf 100644 --- a/specification/sections/04-hooks.md +++ b/specification/sections/04-hooks.md @@ -31,15 +31,21 @@ Hooks can be configured to run globally (impacting all flag evaluations), per cl ### Definitions -**Hook**: Application author/integrator-supplied logic that is called by the OpenFeature framework at a specific stage. **Stage**: An explicit portion of the flag evaluation lifecycle. e.g. `before` being "before the [resolution](../glossary.md#resolving-flag-values) is run. **Invocation**: A single call to evaluate a flag. `client.getBooleanValue(..)` is an invocation. **API**: The global API singleton. +*Hook**: Application author/integrator-supplied logic that is called by the OpenFeature framework at a specific stage. + +**Stage**: An explicit portion of the flag evaluation lifecycle. e.g. `before` being "before the [resolution](../glossary.md#resolving-flag-values) is run. + +**Invocation**: A single call to evaluate a flag. `client.getBooleanValue(..)` is an invocation. + +**API**: The global API singleton. ### 4.1. Hook context -Hook context exists to provide hooks with information about the invocation. +Hook context exists to provide hooks with information about the invocation and propagate data between hook stages. #### Requirement 4.1.1 -> Hook context **MUST** provide: the `flag key`, `flag value type`, `evaluation context`, and the `default value`. +> Hook context **MUST** provide: the `flag key`, `flag value type`, `evaluation context`, `default value`, and `hook data`. #### Requirement 4.1.2 @@ -59,6 +65,22 @@ see: [dynamic-context paradigm](../glossary.md#dynamic-context-paradigm) > The evaluation context **MUST** be mutable only within the `before` hook. +#### Requirement 4.1.5 + +> The `hook data` **MUST** be mutable. + +Either the `hook data` reference itself must be mutable, or it must allow mutation of its contents. + +Mutable reference: +``` +hookContext.hookData = {'my-key': 'my-value'} +``` + +Mutable content: +``` +hookContext.hookData.set('my-key', 'my-value') +``` + ### 4.2. Hook Hints #### Requirement 4.2.1 @@ -87,6 +109,58 @@ see: [dynamic-context paradigm](../glossary.md#dynamic-context-paradigm) > Hooks **MUST** specify at least one stage. +#### Requirement 4.3.2 + +> `Hook data` **MUST** must be created before the first `stage` invoked in a hook for a specific evaluation and propagated between each `stage` of the hook. The hook data is not shared between different hooks. + +Example showing data between `before` and `after` stage for two different hooks. +```mermaid +sequenceDiagram +actor Application +participant Client +participant HookA +participant HookB + +Application->>Client: getBooleanValue('my-bool', myContext, false) +activate Client + +Client-->>Client: create hook data for HookA + +Client->>HookA: before(hookContext: {data: {}, ... }) +activate HookA + +HookA-->>HookA: hookContext.hookData.set('key',' data for A') + +HookA-->>Client: (return) +deactivate HookA + +Client-->>Client: create hook data for HookB + +Client->>HookB: before(hookContext: {data: {}, ... }, hints) +activate HookB + +HookB-->>HookB: hookContext.hookData.set('key', 'data for B') +deactivate HookB + +Client-->>Client: Flag evaluation + +Client->>HookB: after(hookContext: {data: {key: 'data for B'}, ... }, detail, hints) +activate HookB + +HookB-->>Client: (return) +deactivate HookB + +Client->>HookA: after(hookContext: {data: {'key': 'data for A'}, ... }) +activate HookA + +HookA-->>Client: (return) +deactivate HookA + +Client-->>Application: true +deactivate Client + +``` + #### Condition 4.3.2 > The implementation uses the dynamic-context paradigm. @@ -225,3 +299,33 @@ see: [Flag evaluation options](./01-flag-evaluation.md#evaluation-options) #### Requirement 4.5.3 > The hook **MUST NOT** alter the `hook hints` structure. + +### 4.6. Hook data + +Hook data exists to allow hook stages to share data for a specific evaluation. For instance a span +for OpenTelemetry could be created in a `before` stage and closed in an `after` stage. + +Hook data is scoped to a specific hook instance. The different stages of a hook share the same data, +but different hooks have different hook data instances. + +```Java + public Optional before(HookContext context, HookHints hints) { + SpanBuilder builder = tracer.spanBuilder('sample') + .setParent(Context.current().with(Span.current())); + Span span = builder.startSpan() + context.hookData.set("span", span); + } + + public void after(HookContext context, FlagEvaluationDetails details, HookHints hints) { + // Only accessible by this hook for this specific evaluation. + Object value = context.hookData.get("span"); + if (value instanceof Span) { + Span span = (Span) value; + span.end(); + } + } +``` + +#### Requirement 4.6.1 + +> `hook data` **MUST** be a structure supports definition of arbitrary properties, with keys of type `string`, and values of type `boolean | string | number | datetime | structure`.