mirror of https://github.com/open-feature/spec.git
Compare commits
170 Commits
Author | SHA1 | Date |
---|---|---|
|
969e11c4d5 | |
|
c4babd3051 | |
|
baec39b3fe | |
|
2d4b27b116 | |
|
1e98b79719 | |
|
224b26e44e | |
|
a3678719bf | |
|
705d086779 | |
|
c37ac17c80 | |
|
1965aae810 | |
|
42340bb9f5 | |
|
cbfa0a9a78 | |
|
bb2dc2ce32 | |
|
f0148060e6 | |
|
edf0debe0b | |
|
c66684f030 | |
|
d27e000b6c | |
|
2ba05d89b5 | |
|
a5ba017a9c | |
|
36944c68dd | |
|
18cde1708e | |
|
27e4461b45 | |
|
130df3eb61 | |
|
aad6193d77 | |
|
09aef37063 | |
|
25c57ee7b6 | |
|
0cd553d85f | |
|
a69f748db2 | |
|
54952f3b54 | |
|
be56f22af9 | |
|
95fe981d9e | |
|
8d6eeb3247 | |
|
5b07065985 | |
|
6c673d7716 | |
|
c287b5844a | |
|
d261f68331 | |
|
435b11a9b2 | |
|
ed0f9ef5a3 | |
|
776ee1f396 | |
|
d4a9a91094 | |
|
6ecbbfd004 | |
|
7a78eed656 | |
|
3c737a6e86 | |
|
ffebdecd72 | |
|
cd99c3560a | |
|
df1f62ee7f | |
|
807c0d2c8d | |
|
db0df51ce8 | |
|
80ceaa0465 | |
|
7f7830b5d8 | |
|
50b09898ca | |
|
bcb8cf1638 | |
|
464bd02924 | |
|
eaa44da211 | |
|
3f133fb78e | |
|
d46e6dce67 | |
|
e368a5c10b | |
|
8f2fa81b2e | |
|
bc2d6dfc63 | |
|
c0739a164d | |
|
9a80acf637 | |
|
681f38a57b | |
|
8c4d67b550 | |
|
5f262bdd12 | |
|
1513e9a021 | |
|
37cf68b0d6 | |
|
57829b0c4f | |
|
31da456d95 | |
|
ae286cf519 | |
|
a77f18b1ae | |
|
2c36abdc54 | |
|
b58c3b4ec6 | |
|
1889aa799a | |
|
dc29976ef1 | |
|
ffd555254a | |
|
738bb41a3e | |
|
a0c79d7e00 | |
|
afe1c7ea72 | |
|
2ad67fc62a | |
|
c72b95177c | |
|
d071994f0a | |
|
e0ebcbbc22 | |
|
d04fd35d34 | |
|
1b25be11cd | |
|
db20fee6b9 | |
|
bfdc8d1940 | |
|
233094b0f3 | |
|
c21077b43b | |
|
b1edf2a385 | |
|
1bb141b1df | |
|
c75eb75523 | |
|
262da8f7cf | |
|
5ed6c0cc08 | |
|
c4b66e4f39 | |
|
73922b2efb | |
|
2973c53f0f | |
|
816601734a | |
|
34e0a33abb | |
|
814e332c6e | |
|
d0a87da15f | |
|
e2eb2b48c1 | |
|
a0b3777909 | |
|
230bad2f99 | |
|
f45bb276e1 | |
|
cd42c52bd1 | |
|
4b21642564 | |
|
8e8c344796 | |
|
2a2539d2ba | |
|
007502bb37 | |
|
2c9344c443 | |
|
77e0b9c69a | |
|
975a908d8d | |
|
9b86728f57 | |
|
bf9e45b1d5 | |
|
a4ffec3d44 | |
|
b50883c294 | |
|
4cf8229d23 | |
|
c74f409c28 | |
|
a4ff004b2b | |
|
00542fc5d4 | |
|
ea451a9964 | |
|
2108f87e4c | |
|
74c373e089 | |
|
e42dafc8ca | |
|
7ed7442c40 | |
|
8ebe27b11c | |
|
928d513667 | |
|
136c26af80 | |
|
5270ae8770 | |
|
7f0fdd8e84 | |
|
c49b306035 | |
|
0f1561ddf4 | |
|
bb3c21a189 | |
|
3445d95777 | |
|
c1792369e3 | |
|
18d2f02d89 | |
|
a5b7b280e7 | |
|
73430ed040 | |
|
ed9a4bc01b | |
|
29d6378ee7 | |
|
73e1ed6656 | |
|
3b800fd9b0 | |
|
e2629c1873 | |
|
f74a8d9876 | |
|
e6434df8b2 | |
|
9f6563e457 | |
|
ddd5539c0e | |
|
3fcb85678d | |
|
186741ebc2 | |
|
3da57e489e | |
|
86cd353eef | |
|
901dd3a46d | |
|
b3a0cc6e89 | |
|
c5b7b28d73 | |
|
0b272d5822 | |
|
9bc54eb37f | |
|
946be25b3c | |
|
678e49280f | |
|
607e74b5cf | |
|
a97fe2c3af | |
|
8c24a8d900 | |
|
c18fb0eb75 | |
|
7d4648cb96 | |
|
505c26386f | |
|
4ee9082d31 | |
|
f1ca6b9a23 | |
|
2869a85bc2 | |
|
057a80fc19 | |
|
9512910b7c | |
|
c6a4f3a53a |
|
@ -1 +0,0 @@
|
|||
* @oleg-nenashev @toddbaert
|
|
@ -14,8 +14,8 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
|
||||
- name: Generate JSON files
|
||||
run: make parse
|
||||
|
@ -34,8 +34,8 @@ jobs:
|
|||
json-lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
|
||||
- name: Lint
|
||||
run: make lint
|
||||
|
@ -44,11 +44,11 @@ jobs:
|
|||
markdown-toc:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v3
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18"
|
||||
node-version: "22"
|
||||
|
||||
- name: Generate ToC
|
||||
run: make markdown-toc
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
name: PR Python checks
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
WORKING_DIR: tools/repo_parser
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.13
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: ${{ env.WORKING_DIR }}
|
||||
run: pip install -r requirements.txt
|
||||
|
||||
- name: Format
|
||||
working-directory: ${{ env.WORKING_DIR }}
|
||||
run: ruff format --check
|
||||
- name: Lint
|
||||
working-directory: ${{ env.WORKING_DIR }}
|
||||
run: ruff check
|
||||
- name: Typing
|
||||
working-directory: ${{ env.WORKING_DIR }}
|
||||
run: mypy
|
||||
- name: Unit tests
|
||||
working-directory: ${{ env.WORKING_DIR }}
|
||||
run: pytest
|
|
@ -0,0 +1,21 @@
|
|||
# Uses https://github.com/jgehrcke/github-repo-stats to overcome the 14-day limitation of GitHub's built-in traffic statistics.
|
||||
name: "Repo Stats"
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run this once per day, towards the end of the day for keeping the most
|
||||
# recent data point most meaningful (hours are interpreted in UTC).
|
||||
- cron: "0 23 * * *"
|
||||
workflow_dispatch: # Allow for running this manually.
|
||||
|
||||
jobs:
|
||||
snapshot:
|
||||
name: github-repo-stats
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: run-ghrs
|
||||
# Use latest release.
|
||||
uses: jgehrcke/github-repo-stats@v1.4.2
|
||||
with:
|
||||
databranch: github-repo-stats
|
||||
ghtoken: ${{ secrets.REPO_STATS_TOKEN }}
|
|
@ -1,2 +1,3 @@
|
|||
node_modules
|
||||
__pycache__
|
||||
.idea
|
||||
|
|
|
@ -6,3 +6,7 @@ MD033:
|
|||
- details
|
||||
MD024:
|
||||
siblings_only: true
|
||||
MD025:
|
||||
front_matter_title: ""
|
||||
MD028: false # no blank lines in block quote - disabling b/c it fires on 2 consecutive block quotes :/
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
/specification/ @open-feature/technical-steering-committee
|
9
Makefile
9
Makefile
|
@ -1,16 +1,13 @@
|
|||
IS_PYTHON_INSTALLED = $(shell which python >> /dev/null 2>&1; echo $$?)
|
||||
ALL_DOCS := $(shell find . -type f -name '*.md' -not -path './.github/*' -not -path './node_modules/*' | sort)
|
||||
|
||||
parse: clean _check_python
|
||||
parse: _check_python
|
||||
@python ./tools/specification_parser/specification_parser.py
|
||||
|
||||
clean:
|
||||
@find ./specification -name '*.json' -delete
|
||||
|
||||
lint: node_modules
|
||||
@python ./tools/specification_parser/lint_json_output.py specification.json
|
||||
./node_modules/.bin/markdownlint --ignore node_modules/ --ignore tools/ **/*.md
|
||||
./node_modules/.bin/markdown-link-check -c .markdown-link-check-config.json README.md specification/*.md
|
||||
./node_modules/.bin/markdown-link-check -c .markdown-link-check-config.json README.md specification/*.md specification/**/*.md
|
||||
|
||||
fix: node_modules
|
||||
prettier -w **/*.md
|
||||
|
@ -25,6 +22,7 @@ _check_python:
|
|||
&& echo "" \
|
||||
&& exit 1; \
|
||||
fi;
|
||||
|
||||
.PHONY: markdown-toc
|
||||
markdown-toc: node_modules
|
||||
@if ! npm ls markdown-toc; then npm ci; fi
|
||||
|
@ -32,6 +30,7 @@ markdown-toc: node_modules
|
|||
if grep -q '<!-- tocstop -->' $$f; then \
|
||||
echo markdown-toc: processing $$f; \
|
||||
npx --no -- markdown-toc --bullets="-" --no-first-h1 --no-stripHeadingTags -i $$f || exit 1; \
|
||||
npx --no -- prettier -w $$f; \
|
||||
else \
|
||||
echo markdown-toc: no TOC markers, skipping $$f; \
|
||||
fi; \
|
||||
|
|
58
README.md
58
README.md
|
@ -1,29 +1,55 @@
|
|||
# OpenFeature Specification (Draft)
|
||||
<!-- markdownlint-disable MD033 -->
|
||||
<!-- x-hide-in-docs-start -->
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/open-feature/community/0e23508c163a6a1ac8c0ced3e4bd78faafe627c7/assets/logo/horizontal/white/openfeature-horizontal-white.svg" />
|
||||
<img align="center" alt="OpenFeature Logo" src="https://raw.githubusercontent.com/open-feature/community/0e23508c163a6a1ac8c0ced3e4bd78faafe627c7/assets/logo/horizontal/black/openfeature-horizontal-black.svg" />
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
[](https://github.com/orgs/open-feature/projects/1) [](https://github.com/open-feature/.github/blob/main/CONTRIBUTING.md) [](https://github.com/open-feature/.github/blob/main/CODE_OF_CONDUCT.md)
|
||||
<h2 align="center">OpenFeature Specification</h2>
|
||||
|
||||
<!-- x-hide-in-docs-end -->
|
||||
<!-- The 'github-badges' class is used in the docs -->
|
||||
<p align="center" class="github-badges">
|
||||
<a href="https://github.com/orgs/open-feature/projects/1">
|
||||
<img alt="Roadmap" src="https://img.shields.io/static/v1?label=Roadmap&message=public&color=green" />
|
||||
</a>
|
||||
<a href="https://github.com/open-feature/.github/blob/main/CONTRIBUTING.md">
|
||||
<img alt="Contributing" src="https://img.shields.io/static/v1?label=Contributing&message=guide&color=blue" />
|
||||
</a>
|
||||
<a href="https://cloud-native.slack.com/archives/C0344AANLA1">
|
||||
<img alt="Slack" src="https://img.shields.io/badge/slack-%40cncf%2Fopenfeature-brightgreen?style=flat&logo=slack"/>
|
||||
</a>
|
||||
<a href="https://bestpractices.coreinfrastructure.org/projects/6601">
|
||||
<img alt="CII Best Practices" src="https://bestpractices.coreinfrastructure.org/projects/6601/badge" />
|
||||
</a>
|
||||
</p>
|
||||
<!-- x-hide-in-docs-start -->
|
||||
|
||||
[OpenFeature](https://openfeature.dev) is an open specification that provides a vendor-agnostic, community-driven API for feature flagging that works with your favorite feature flag management tool or in-house solution.
|
||||
|
||||
This repository describes the requirements and expectations for OpenFeature.
|
||||
|
||||
> :warning: Ongoing research can be found in the [research repo](https://github.com/open-feature/research). For definitions of key terminology, see the [glossary](./specification/glossary.md).
|
||||
|
||||
## Design Principles
|
||||
|
||||
- Compatibility with existing feature flag offerings
|
||||
- Simple, understandable APIs
|
||||
- Vendor agnosticism
|
||||
- Language agnosticism
|
||||
- Low/no dependency
|
||||
- Extensibility
|
||||
The OpenFeature specification must be designed with:
|
||||
|
||||
- compatibility with existing feature flag offerings.
|
||||
- simple, understandable APIs.
|
||||
- vendor agnosticism.
|
||||
- language agnosticism.
|
||||
- low/no dependency.
|
||||
- extensibility.
|
||||
|
||||
### SDKs and Client Libraries
|
||||
|
||||
The project aims to provide a unified API and SDK for feature flag management in various technology stacks. The flag evaluation logic will **not** be handled in the OpenFeature SDK itself but provide a mechanism for interfacing with an external evaluation engine in a vendor agnostic way.
|
||||
The project aims to provide a unified API and SDK feature flag evaluation across popular technology stacks.
|
||||
The OpenFeature SDK provides a mechanism for interfacing
|
||||
with an external evaluation engine in a vendor agnostic way;
|
||||
it does **not** itself handle the flag evaluation logic.
|
||||
|
||||
The OpenFeature project will include client libraries for common technology stacks including, but not limited to:
|
||||
|
||||
- Golang
|
||||
- Java
|
||||
- JavaScript/TypeScript (Node.js)
|
||||
An up-to-date SDK compatibility overview can be found [here](https://openfeature.dev/docs/reference/technologies/sdk-compatibility).
|
||||
|
||||
### Tooling
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -4,7 +4,7 @@
|
|||
"devDependencies": {
|
||||
"markdown-link-check": "^3.10.2",
|
||||
"markdown-toc": "^1.2.0",
|
||||
"markdownlint-cli": "^0.31.1",
|
||||
"prettier": "^2.6.2"
|
||||
"markdownlint-cli": "^0.44.0",
|
||||
"prettier": "^3.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended",
|
||||
":automergeTypes",
|
||||
":automergeStableNonMajor",
|
||||
"npm:unpublishSafe"
|
||||
],
|
||||
"semanticCommits": "enabled",
|
||||
"labels": [
|
||||
"renovate"
|
||||
]
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,13 +1,26 @@
|
|||
---
|
||||
id: intro
|
||||
title: Introduction
|
||||
description: An introduction to the OpenFeature specification.
|
||||
sidebar_position: 0
|
||||
---
|
||||
|
||||
# OpenFeature Specification
|
||||
|
||||
## Contents
|
||||
|
||||
- [Glossary](./glossary.md)
|
||||
- [Types](./types.md)
|
||||
- [Evaluation API](./flag-evaluation.md)
|
||||
- [Providers](./providers.md)
|
||||
- [Evaluation Context](./evaluation-context.md)
|
||||
- [Hooks](./hooks.md)
|
||||
- [Evaluation API](./sections/01-flag-evaluation.md)
|
||||
- [Providers](./sections//02-providers.md)
|
||||
- [Evaluation Context](./sections/03-evaluation-context.md)
|
||||
- [Hooks](./sections/04-hooks.md)
|
||||
- [Events](./sections/05-events.md)
|
||||
- [Tracking](./sections/06-tracking.md)
|
||||
- [Appendix A: Included Utilities](./appendix-a-included-utilities.md)
|
||||
- [Appendix B: Gherkin Suites](./appendix-b-gherkin-suites.md)
|
||||
- [Appendix C: OFREP](./appendix-c-ofrep.md)
|
||||
- [Appendix D: Observability](./appendix-d-observability.md)
|
||||
|
||||
## Conformance
|
||||
|
||||
|
@ -28,7 +41,42 @@ An implementation is not compliant if it fails to satisfy one or more of the "MU
|
|||
|
||||
## Document Statuses
|
||||
|
||||
| Status | Explanation |
|
||||
| -------------------- | ----------------------------- |
|
||||
| No Explicit "Status" | Equivalent to Experimental. |
|
||||
| Experimental | Breaking changes are allowed. |
|
||||
Sections and subsections within the specification are marked with statuses indicating their stability level.
|
||||
Functionality described in the specification graduates through these statuses with increasing stability.
|
||||
Stability levels apply only to normative sections within the specification; editorial changes to examples and explanations are exempt from these constraints.
|
||||
It is the responsibility of the [Technical Steering Committee](https://github.com/open-feature/community/blob/main/governance-charter.md#tsc-members) to consider and approve the graduation of documents.
|
||||
|
||||
Possible statuses are described below:
|
||||
|
||||
### Experimental
|
||||
|
||||
[](https://github.com/open-feature/spec/tree/main/specification#experimental)
|
||||
|
||||
Specification sections that are marked as `Experimental` contain functionality under active development. Breaking changes are allowed and may be made without deprecation notices or warnings with minor version updates. We recommend you use these features in experimental environments and not in production.
|
||||
|
||||
Put simply:
|
||||
|
||||
> We're testing these features out. Things could change anytime.
|
||||
|
||||
### Hardening
|
||||
|
||||
[](https://github.com/open-feature/spec/tree/main/specification#hardening)
|
||||
|
||||
Sections marked as `Hardening` describe functionality with an emphasis on stabilizing existing requirements. Breaking changes require consensus by the [Technical Steering Committee](https://github.com/open-feature/community/blob/main/governance-charter.md#tsc-members) but may still be made with minor version updates. These features are suitable for use in production environments. Feedback is encouraged.
|
||||
|
||||
Put simply:
|
||||
|
||||
> We believe these features are ready for production use, and hope for feedback.
|
||||
|
||||
### Stable
|
||||
|
||||
[](https://github.com/open-feature/spec/tree/main/specification#stable)
|
||||
|
||||
Sections marked as `Stable` do not allow breaking changes without a major version update. They can be used in production with a high degree of confidence.
|
||||
|
||||
Put simply:
|
||||
|
||||
> These features are stable and battle-hardened.
|
||||
|
||||
> [!NOTE]
|
||||
> No explicit status = `Experimental`
|
||||
|
|
|
@ -0,0 +1,435 @@
|
|||
---
|
||||
id: appendix-a
|
||||
title: "Appendix A: Included Utilities"
|
||||
description: Information on OpenFeature ecosystem utilities
|
||||
sidebar_position: 4
|
||||
---
|
||||
|
||||
# Appendix A: Included Utilities
|
||||
|
||||
This document contains requirements for auxiliary utilities provided by the SDK, such as testing utilities.
|
||||
|
||||
## In-memory provider
|
||||
|
||||
> OpenFeature SDK implementations **SHOULD** provide an `in-memory provider`.
|
||||
|
||||
The in-memory provider is intended to be used for testing; SDK consumers may use it for their use cases.
|
||||
Hence, the packaging, naming, and access modifiers must be set appropriately.
|
||||
|
||||
This provider **MUST** have the following features:
|
||||
|
||||
- The provider is initiated with a pre-defined `flag set` provided in the constructor.
|
||||
- The flag structure must be minimalist and should help test the OpenFeature specification.
|
||||
- EvaluationContext support should be provided through callbacks/lambda expressions.
|
||||
- The provider must support a means of updating the `flag set`, resulting in the emission of `PROVIDER_CONFIGURATION_CHANGED` events.
|
||||
- The change event should consider all flags changed; a union of all previous and all new flag keys should be supplied in the `flags changed` field.
|
||||
- The provider must be maintained to support specification changes.
|
||||
|
||||
## SDK end-to-end testing
|
||||
|
||||
> E2E tests must utilize [in-memory provider](#in-memory-provider) defined within the SDK and must be self-contained.
|
||||
|
||||
OpenFeature project maintains an end-to-end(e2e) test suite defined with [Gherkin syntax](https://cucumber.io/docs/gherkin/).
|
||||
These test definitions reside in [Appendix B](./appendix-b-gherkin-suites.md)
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph SDK
|
||||
A[e2e Tests] -.-> B[In-memory provider]
|
||||
end
|
||||
```
|
||||
|
||||
## Multi-Provider
|
||||
|
||||
### Introduction
|
||||
|
||||
The OpenFeature Multi-Provider wraps multiple underlying providers in a unified interface, allowing the SDK client to transparently interact with all those providers at once.
|
||||
This allows use cases where a single client and evaluation interface is desired, but where the flag data should come from more than one source, or where tracking events should be sent to multiple providers simultaneously.
|
||||
|
||||
Some examples:
|
||||
|
||||
- **Migration**: When migrating between two providers, you can run both in parallel under a unified flagging interface. As flags are added to the new provider, the Multi-Provider will fetch flags from the new provider first, falling back to the old provider.
|
||||
- **Multiple Data Sources**: The Multi-Provider allows you to combine many sources of flagging data, such as environment variables, local files, database values and SaaS hosted feature management systems.
|
||||
- **Analytics Aggregation**: Send tracking events to multiple analytics providers simultaneously.
|
||||
|
||||
Check the [OpenFeature JavaScript Multi-Provider](https://github.com/open-feature/js-sdk-contrib/tree/main/libs/providers/multi-provider) for a reference implementation.
|
||||
|
||||
### Basics
|
||||
|
||||
The provider is initialized by passing a list of provider instances it should evaluate.
|
||||
The order of the array defines the order in which sources should be evaluated.
|
||||
The provider whose value is ultimately used will depend on the "strategy" that is provided, which can be chosen from a set of pre-defined ones or implemented as custom logic.
|
||||
|
||||
For example:
|
||||
|
||||
```typescript
|
||||
const multiProvider = new MultiProvider(
|
||||
[
|
||||
{
|
||||
provider: new ProviderA(),
|
||||
},
|
||||
{
|
||||
provider: new ProviderB()
|
||||
}
|
||||
],
|
||||
new FirstMatchStrategy()
|
||||
)
|
||||
|
||||
await OpenFeature.setProviderAndWait(multiProvider)
|
||||
```
|
||||
|
||||
From the perspective of the SDK client, this provider will now act as a "normal" spec-compliant provider, while handling the complexities of aggregating multiple providers internally.
|
||||
|
||||
### Specific Behavior
|
||||
|
||||
When dealing with many providers at once, various aspects of those providers need to be "combined" into one unified view.
|
||||
For example, each internal provider has a "status", which should influence the Multi-Provider's overall "status".
|
||||
The specific aspects that need to be addressed are described below.
|
||||
|
||||
#### Unique Names
|
||||
|
||||
In order to identify each provider uniquely, it must have a name associated with it.
|
||||
The unique name will be used when reporting errors and results in order to indicate from which provider they came from.
|
||||
|
||||
Most providers have a `metadata.name` field which could be used, but this would be non-unique in the case where two instances of the same type of provider are used. As a result there would need to be a way to differentiate the two.
|
||||
|
||||
When instantiating the Multi-Provider, there will be an option for specifying a name to associate to each provider:
|
||||
|
||||
```typescript
|
||||
const multiProvider = new MultiProvider([
|
||||
{
|
||||
provider: new ProviderA(),
|
||||
name: "ProviderA"
|
||||
},
|
||||
{
|
||||
provider: new ProviderB(),
|
||||
name: "ProviderB"
|
||||
}
|
||||
])
|
||||
|
||||
await OpenFeature.setProviderAndWait(multiProvider)
|
||||
```
|
||||
|
||||
Names for each provider are then determined like this:
|
||||
|
||||
1. name passed in to constructor if specified
|
||||
2. `metadata.name` if it is unique among providers
|
||||
3. `${metadata.name}_${index}` if name is not unique. Eg. the first instance of ProviderA provider might be named "providerA_1" and the second might be "providerA_2"
|
||||
|
||||
If multiple names are passed in the constructor which conflict, an error will be thrown.
|
||||
|
||||
#### Initialization
|
||||
|
||||
Initialization of each provider should be handled in parallel in the Multi-Provider's `initialize` method.
|
||||
It should call `initialize` on each provider it is managing, and bubble up any error that is thrown by re-throwing to the client.
|
||||
|
||||
#### Status and Event Handling
|
||||
|
||||
The status of a provider is tracked in OpenFeature SDKs based on emitted events.
|
||||
|
||||
Provider states can be transitioned in the ways represented here:
|
||||
[https://openfeature.dev/specification/sections/flag-evaluation#17-provider-lifecycle-management](https://openfeature.dev/specification/sections/flag-evaluation#17-provider-lifecycle-management)
|
||||
|
||||
The SDK client tracks statuses of a provider as follows:
|
||||
|
||||
- Initially the status is `NOT_READY`
|
||||
- Initialize function is called (if exists) and result is awaited
|
||||
- Successful initialize transitions state to `READY`, error result transitions to either `ERROR` or `FATAL`
|
||||
- From this point onwards, status is only changed as a result of provider emitting events to indicate status-changing things have occurred.
|
||||
- It can emit events like `FATAL`, `ERROR`, `STALE` and `READY` to transition to those states.
|
||||
|
||||
The only statuses which affect evaluation behavior at the SDK client level are `FATAL` and `NOT_READY`.
|
||||
If a provider is in either of these states, evaluation will be "skipped" by the client and the default value will be returned.
|
||||
|
||||
Other statuses are currently "informational". Nevertheless, the Multi-Provider will represent an overall "status" based on the combined statuses of the providers.
|
||||
|
||||
##### Multi-Provider Status
|
||||
|
||||
The Multi-Provider mimics the event handling logic that tracks statuses in the SDK, and keeps track of the status of each provider it is managing.
|
||||
The individual status-changing events from these providers will be "captured" in the Multi-Provider, and not re-emitted to the outer SDK UNLESS they cause the status of the Multi-Provider to change.
|
||||
The status of the Multi-Provider will change when one of its providers changes to a status that is considered higher "precedence" than the current status.
|
||||
|
||||
The precedence order is defined as:
|
||||
|
||||
- FATAL
|
||||
- NOT_READY
|
||||
- ERROR
|
||||
- STALE
|
||||
- READY
|
||||
|
||||
For example, if all providers are currently in `READY` status, the Multi-Provider will be in `READY` status.
|
||||
If one of the providers is `STALE`, the status of the Multi-Provider will be `STALE`.
|
||||
If a different provider now becomes `ERROR`, the status will be `ERROR` even if the other provider is still in `STALE`.
|
||||
|
||||
When the Multi-Provider changes status, it does so by emitting the appropriate event to the SDK.
|
||||
The "details" of that event will be **identical** to the details of the original event from one of the inner providers which triggered this state change.
|
||||
|
||||
There is another event called "configuration changed" which does not affect status.
|
||||
This event should be re-emitted any time it occurs from any provider.
|
||||
|
||||
#### Evaluation Result
|
||||
|
||||
The evaluation result is based on the results from evaluating each provider.
|
||||
There are multiple "strategies" configurable in the Multi-Provider to decide how to use the results.
|
||||
|
||||
#### Track Method Support
|
||||
|
||||
The Multi-Provider implements the `track` method from the OpenFeature specification, allowing tracking events to be sent across multiple underlying providers.
|
||||
|
||||
By default, tracking events are sent to all providers that are in `READY` status.
|
||||
Providers in `NOT_READY` or `FATAL` states are automatically skipped for tracking operations.
|
||||
|
||||
Key features of tracking support include:
|
||||
|
||||
- **Error Resilience**: Individual provider tracking failures do not break the overall tracking flow - errors are logged but do not throw exceptions
|
||||
- **Status Awareness**: Providers in `NOT_READY` or `FATAL` status will not recieve tracking events.
|
||||
- **Strategy Integration**: Custom strategies can control which providers receive tracking calls using the `shouldTrackWithThisProvider` method
|
||||
- **Graceful Degradation**: Providers that don't implement the `track` method are skipped
|
||||
|
||||
Example usage:
|
||||
|
||||
```typescript
|
||||
const multiProvider = new MultiProvider([
|
||||
{ provider: new ProviderA() },
|
||||
{ provider: new ProviderB() }
|
||||
])
|
||||
|
||||
await OpenFeature.setProviderAndWait(multiProvider)
|
||||
const client = OpenFeature.getClient()
|
||||
|
||||
// Track events across all ready providers
|
||||
client.track('purchase', { targetingKey: 'user123' }, { value: 99.99, currency: 'USD' })
|
||||
```
|
||||
|
||||
For custom tracking behavior, strategies can implement the `shouldTrackWithThisProvider` method to selectively track with specific providers based on event name, context, or provider characteristics.
|
||||
|
||||
#### Interpreting Errors
|
||||
|
||||
Currently, providers have multiple ways of signalling evaluation errors to the SDK.
|
||||
Particularly in the case of Javascript, a provider can return an evaluation result that contains an error code and message, but still has a "value" for the result. It can also throw an error.
|
||||
|
||||
Several providers currently use the former approach for indicating errors in operations, and use the `value` field of the result to return the default value from the provider itself.
|
||||
|
||||
For the purposes of aggregating providers, the Multi-Provider treats both thrown and returned errors as an "error" result. If the returned error result has a value, that value will be ignored by all strategies. Only "nominal" evaluation results will be considered by the evaluation.
|
||||
|
||||
### Strategies
|
||||
|
||||
The Multi-Provider supports multiple ways of deciding how to evaluate the set of providers it is managing, and how to deal with any errors that are thrown.
|
||||
|
||||
Strategies must be adaptable to the various requirements that might be faced in a multi-provider situation.
|
||||
In some cases, the strategy may want to ignore errors from individual providers as long as one of them successfully responds.
|
||||
In other cases, it may want to evaluate providers in order and skip the rest if a successful result is obtained.
|
||||
In still other scenarios, it may be required to always call every provider and decide what to do with the set of results.
|
||||
|
||||
The strategy to use is passed in to the Multi-Provider constructor as follows:
|
||||
|
||||
```typescript
|
||||
new MultiProvider(
|
||||
[
|
||||
{
|
||||
provider: new ProviderA()
|
||||
},
|
||||
{
|
||||
provider: new ProviderB()
|
||||
}
|
||||
],
|
||||
new FirstMatchStrategy()
|
||||
)
|
||||
```
|
||||
|
||||
By default, the Multi-Provider uses the `FirstMatchStrategy`.
|
||||
|
||||
Here are some standard strategies that come with the Multi-Provider:
|
||||
|
||||
#### First Match Strategy
|
||||
|
||||
Return the first result returned by a provider.
|
||||
Skip providers that indicate they had no value due to `FLAG_NOT_FOUND`.
|
||||
In all other cases, use the value returned by the provider.
|
||||
If any provider returns an error result other than `FLAG_NOT_FOUND`, the whole evaluation should error and "bubble up" the individual provider's error in the result.
|
||||
|
||||
As soon as a value is returned by a provider, the rest of the operation should short-circuit and not call the rest of the providers.
|
||||
|
||||
[See the refrence implementation](https://github.com/open-feature/js-sdk-contrib/blob/main/libs/providers/multi-provider/src/lib/strategies/FirstMatchStrategy.ts)
|
||||
|
||||
#### First Successful Strategy
|
||||
|
||||
Similar to "First Match", except that errors from evaluated providers do not halt execution.
|
||||
Instead, it will return the first successful result from a provider. If no provider successfully responds, it will throw an error result.
|
||||
|
||||
[See the refrence implementation](https://github.com/open-feature/js-sdk-contrib/blob/main/libs/providers/multi-provider/src/lib/strategies/FirstSuccessfulStrategy.ts)
|
||||
|
||||
#### Comparison Strategy
|
||||
|
||||
Require that all providers agree on a value.
|
||||
If every provider returns a non-error result, and the values do not agree, the Multi-Provider should return the result from a configurable "fallback" provider.
|
||||
It will also call an optional "onMismatch" callback that can be used to monitor cases where mismatches of evaluation occurred.
|
||||
Otherwise the value of the result will be the result of the first provider in precedence order.
|
||||
|
||||
[See the refrence implementation](https://github.com/open-feature/js-sdk-contrib/blob/main/libs/providers/multi-provider/src/lib/strategies/ComparisonStrategy.ts)
|
||||
|
||||
#### User Defined Custom Strategy
|
||||
|
||||
Rather than making assumptions about when to use a provider's result and when not to (which may not hold across all providers) there is also a way for the user to define their own strategy that determines whether or not to use a result or fall through to the next one.
|
||||
|
||||
A strategy can be implemented by implementing the `BaseEvaluationStrategy` class as follows:
|
||||
|
||||
```typescript
|
||||
type StrategyEvaluationContext = {
|
||||
flagKey: string;
|
||||
flagType: FlagValueType;
|
||||
};
|
||||
|
||||
type StrategyPerProviderContext = StrategyEvaluationContext & {
|
||||
provider: Provider;
|
||||
providerName: string;
|
||||
providerStatus: ProviderStatus;
|
||||
};
|
||||
|
||||
type ProviderResolutionResult<T extends FlagValue> = {
|
||||
details: ResolutionDetails<T>;
|
||||
thrownError?: unknown;
|
||||
provider: Provider;
|
||||
providerName: string;
|
||||
}
|
||||
type FinalResult = {
|
||||
details?: ResolutionDetails<unknown>;
|
||||
provider?: Provider;
|
||||
providerName?: string;
|
||||
errors?: {
|
||||
providerName: string;
|
||||
error: unknown;
|
||||
}[];
|
||||
};
|
||||
|
||||
abstract class BaseEvaluationStrategy {
|
||||
runMode: 'parallel' | 'sequential'
|
||||
|
||||
abstract shouldEvaluateThisProvider(
|
||||
strategyContext: StrategyPerProviderContext,
|
||||
evalContext: EvaluationContext
|
||||
): boolean;
|
||||
|
||||
abstract shouldEvaluateNextProvider<T extends FlagValue>(
|
||||
strategyContext: StrategyPerProviderContext,
|
||||
context: EvaluationContext,
|
||||
result: ProviderResolutionResult<T>
|
||||
): boolean;
|
||||
|
||||
abstract determineFinalResult<T extends FlagValue>(
|
||||
strategyContext: StrategyEvaluationContext,
|
||||
context: EvaluationContext,
|
||||
resolutions: ProviderResolutionResult<T>[],
|
||||
): FinalResult;
|
||||
|
||||
abstract shouldTrackWithThisProvider(
|
||||
strategyContext: StrategyProviderContext,
|
||||
context: EvaluationContext,
|
||||
trackingEventName: string,
|
||||
trackingEventDetails: TrackingEventDetails,
|
||||
): boolean;
|
||||
}
|
||||
```
|
||||
|
||||
- **`runMode`**: property defines whether the providers will all be evaluated at once in `parallel`, or whether they will be evaluated `sequentially` with each result determining whether to evaluate the next one in order.
|
||||
|
||||
- **`shouldEvaluateThisProvider`**: function is called for each provider right before the Multi-Provider would evaluate it.
|
||||
- If the function returns false, the provider will be skipped.
|
||||
- This can be useful in cases where it's desired to skip a provider based on what flag key is being used, or based on some state from the provider itself that indicates it shouldn't be evaluated right now.
|
||||
|
||||
- **`shouldEvaluateNextProvider`**: function is called right after a provider is evaluated.
|
||||
- It is called with the details of resolution or any error that was thrown (which will be caught).
|
||||
- If the function returns true, the next provider will be called.
|
||||
- Otherwise all remaining providers will be skipped and the results of the ones that have been evaluated so far will be passed to `determineFinalResult` .
|
||||
- If this function throws an error, the Multi-Provider will throw an error and not evaluate further providers.
|
||||
- This function is not called when `runMode` is `parallel`, since all providers will be executed (as long as they individually pass the `shouldEvaluateThisProvider` check)
|
||||
|
||||
- **`determineFinalResult`**: function is called after the resolution stage if no further providers will be called.
|
||||
- This function can be used to decide from the set of resolutions which one should ultimately be used.
|
||||
- The function must return a `FinalResult` object which contains the final `ResolutionDetails` and the provider that they correspond to, or an array of `errors` in the case of a non-successful result, with the provider that created each error.
|
||||
|
||||
- **`shouldTrackWithThisProvider`**: function is called when the `track()` method is called on the SDK.
|
||||
- This function can be used to decide which providers should recieve `track()` events.
|
||||
- Return `true` to send the tracking event to the provider, `false` to skip it.
|
||||
|
||||
To see [reference implementations](https://github.com/open-feature/js-sdk-contrib/tree/main/libs/providers/multi-provider/src/lib/strategies) of the above-mentioned strategies.
|
||||
|
||||
### Hooks
|
||||
|
||||
Provider hooks are capable of modifying the context before an evaluation takes place.
|
||||
This behavior must be preserved, but it's also necessary to prevent these hooks from interfering with the context being passed to other providers.
|
||||
|
||||
For this reason, the Multi-Provider manages calling the hooks of each provider itself, at the appropriate time.
|
||||
It then uses the result of the before hooks for a given provider as the new evaluation context when evaluating **that provider**, without affecting the context used for other providers.
|
||||
|
||||
It then calls the after, error and finally hooks using the appropriate context as well.
|
||||
|
||||
Errors thrown from these hooks are be bubbled up to the client, depending on how the evaluation "strategy" defines what to do with errors.
|
||||
|
||||
### Shutdown
|
||||
|
||||
The shutdown method should ensure that `shutdown()` is called in all underlying providers, and bubble up any errors to the client
|
||||
|
||||
### Error Handling
|
||||
|
||||
In cases where all providers are being called (Evaluation etc.) there may be more than one error encountered from more than one provider.
|
||||
The Multi-Provider will collect and throw all errors in an aggregated form as follows:
|
||||
|
||||
```javascript
|
||||
error = {
|
||||
message: 'some message',
|
||||
code: SOME_ERROR,
|
||||
// which provider caused the error
|
||||
originalErrors: [
|
||||
{
|
||||
source: 'ProviderA',
|
||||
error: {
|
||||
message: 'something',
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
In the case where only one error is thrown by one provider, it will still throw in this form for consistency.
|
||||
|
||||
Other errors from the Multi-Provider itself will use standard error types.
|
||||
|
||||
### Metadata
|
||||
|
||||
Providers can contain metadata. The Multi-Provider will make that metadata available within its own metadata as follows:
|
||||
|
||||
```javascript
|
||||
{
|
||||
name: 'multiprovider',
|
||||
originalMetadata: {
|
||||
providerA: {...},
|
||||
providerB: {...}
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Logging Hook
|
||||
|
||||
> OpenFeature SDK implementations **SHOULD** provide a `logging hook`.
|
||||
|
||||
The logging hook is a hook which logs messages during the flag evaluation life-cycle as described below:
|
||||
|
||||
| Stage | Logged data |
|
||||
| ------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| before | `stage`, `domain`, `provider_name`, `flag_key`, `default_value` and `evaluation_context` (serialized, opt-in) |
|
||||
| after | `stage`, `domain`, `provider_name`, `flag_key`, `default_value`, `evaluation_context` (serialized, opt-in), `reason`, `variant` and `value` |
|
||||
| error | `stage`, `domain`, `provider_name`, `flag_key`, `default_value`, `evaluation_context` (serialized, opt-in), `error_code`, and `error_message` |
|
||||
| finally | N/A |
|
||||
|
||||
> The evaluation context **SHOULD** only be logged if an associated option indicates so.
|
||||
|
||||
This can be a constructor option or similar, for example: `boolean printContext`.
|
||||
|
||||
> If logging the evaluation context is enabled, it **MUST** be printed in such a way that it's human readable.
|
||||
|
||||
Consider printing the evaluation context as a stringified JSON object, or using some other format that allows the nested properties to be easily read.
|
||||
|
||||
> If the logger abstraction in the SDK supports a log level concept, the appropriate log level **SHOULD** be used for each stage (before/after: debug/info, error: error).
|
||||
|
||||
Consider using `debug` or `info` levels for the `before` and `after` stages, and the `error` level for the `error` stage.
|
|
@ -0,0 +1,16 @@
|
|||
---
|
||||
id: appendix-b
|
||||
title: "Appendix B: Gherkin Suites"
|
||||
description: A Set of End-to-End Tests for Validating OpenFeature Implementations
|
||||
sidebar_position: 5
|
||||
---
|
||||
|
||||
# Appendix B: Gherkin Suites
|
||||
|
||||
This section contains a set of language-agnostic end-to-end tests (defined in gherkin).
|
||||
These tests can be used to validate the behavior of an OpenFeature implementation.
|
||||
"Features" (test suites) can be used in conjunction with an [in-memory provider](./appendix-a-included-utilities.md#in-memory-provider) and a cucumber test-runner for the language in question.
|
||||
|
||||
## Evaluation Feature
|
||||
|
||||
The [evaluation feature](./assets/gherkin/evaluation.feature) contains tests for the basic functionality of the [Evaluation API](./sections/01-flag-evaluation.md).
|
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
id: appendix-c
|
||||
title: "Appendix C: OpenFeature Remote Evaluation Protocol"
|
||||
description: A unified protocol for feature flagging
|
||||
sidebar_position: 5
|
||||
---
|
||||
|
||||
# Appendix C: OpenFeature Remote Evaluation Protocol
|
||||
|
||||
OpenFeature Remote Evaluation Protocol (OFREP) is a unified feature flag evaluation protocol that adheres to the OpenFeature semantics.
|
||||
You can find proposals, discussions and the progress at the dedicated [OpenFeature OFREP](https://github.com/open-feature/protocol) repository.
|
||||
|
||||
There is a dedicated [OpenFeature working group](https://github.com/open-feature/community/blob/main/config/open-feature/spec-evaluation/workgroup.yaml) for this initiative that focuses on:
|
||||
|
||||
- Feature flag evaluation protocol
|
||||
- Telemetry enrichment of the evaluations
|
||||
- Reference implementations of the protocol
|
||||
|
||||
If you are interested in this initiative, you can join the [`#openfeature-remote-evaluation-protocol`](https://cloud-native.slack.com/archives/C066A48LK35) Slack channel on the [CNCF Slack](https://communityinviter.com/apps/cloud-native/cncf) workspace.
|
|
@ -0,0 +1,73 @@
|
|||
---
|
||||
id: appendix-d
|
||||
title: "Appendix D: Observability"
|
||||
description: Conventions for OpenFeature telemetry signals
|
||||
sidebar_position: 5
|
||||
---
|
||||
|
||||
# Appendix D: Observability
|
||||
|
||||
This document describes conventions for extracting data from the OpenFeature SDK for use in telemetry signals.
|
||||
It primarily focuses on providing recommendations for mapping well-known fields in OpenFeature to [OpenTelemetry feature-flag log records][otel-ff-logs] and other semantic conventions.
|
||||
|
||||
## Evaluations
|
||||
|
||||
Flag evaluation telemetry comprises data resolved from the provider resolution (evaluation details and flag metadata) as well as metadata about the provider itself.
|
||||
This is particularly relevant to telemetry-related [hooks](./sections/04-hooks.md).
|
||||
|
||||
### Evaluation Details
|
||||
|
||||
The following describes how fields on the [evaluation details](types.md#evaluation-details) are mapped to feature flag log records:
|
||||
|
||||
| Log Record Attribute | Source Field or Derived Value from Evaluation Details | Requirement level | Type | Notes |
|
||||
| ----------------------------- | ----------------------------------------------------- | ----------------------------- | ----------- | --------------------------------------------------------------------------------------------------------------------- |
|
||||
| `feature_flag.key` | `flag key` | `Required` | `string` | See: [flag key](./glossary.md#flag-key) |
|
||||
| `feature_flag.result.variant` | `variant` | `Conditionally Required` [^1] | `string` | See: [variant](./glossary.md#variant) |
|
||||
| `feature_flag.result.value` | `value` | `Conditionally Required` [^2] | `undefined` | See: [value](./glossary.md#values) |
|
||||
| `feature_flag.result.reason` | `reason` | `Recommended` | `string` | See: [reason](./types.md#resolution-reason) |
|
||||
| `error.type` | `error code` | `Conditionally Required` [^3] | `string` | See: [error code](./types.md#error-code), |
|
||||
| `error.message` | `error message` | `Conditionally Required` [^3] | `string` | A human-readable error message associated with a failed evaluation. For programmatic purposes, refer to `error code`. |
|
||||
|
||||
> [!NOTE]
|
||||
> The `error.type` and `feature_flag.result.reason` enumerations use a lowercase "snake_case" convention (see [OpenTelemetry feature-flag log records][otel-ff-logs]).
|
||||
> OpenFeature [error codes](types.md#error-code) and [resolution reasons](./types.md#resolution-reason) should be transformed accordingly by integrations which include this data.
|
||||
|
||||
### Flag Metadata
|
||||
|
||||
The following describes how keys in [flag metadata](types.md#flag-metadata) are mapped to feature flag log records:
|
||||
|
||||
| Log Record Attribute | Flag Metadata Key | Requirement level | Type | Notes |
|
||||
| ------------------------- | ----------------- | ----------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `feature_flag.context.id` | `contextId` | `Recommended` | `string` | The context identifier returned in the flag metadata uniquely identifies the subject of the flag evaluation. If not available, the [targeting key](./glossary.md#targeting-key) should be used. |
|
||||
| `feature_flag.set.id` | `flagSetId` | `Recommended` | `string` | A logical identifier for the [flag set](./glossary.md#flag-set). |
|
||||
| `feature_flag.version` | `version` | `Recommended` | `string` | A version string (format unspecified) for the flag or [flag set](./glossary.md#flag-set). |
|
||||
|
||||
> [!NOTE]
|
||||
> Keys in flag metadata use the "camelCase" casing convention, while the OpenTelemetry standard uses a namespaced "snake_case" convention.
|
||||
|
||||
### Provider Metadata
|
||||
|
||||
| Log Record Attribute | Provider Metadata Field | Requirement level | Type | Notes |
|
||||
| ---------------------------- | ----------------------- | ----------------- | -------- | ------------------------------------------------------------------------------------------------ |
|
||||
| `feature_flag.provider.name` | `name` | `Recommended` | `string` | The name of the provider as defined in the `provider metadata`, available in the `hook context`. |
|
||||
|
||||
## History
|
||||
|
||||
Feature flags in the OpenTelemetry semantic conventions are currently in development and are marked as experimental.
|
||||
The following table describes the history of changes to the OpenTelemetry feature flag log records as it progresses towards a stable release.
|
||||
|
||||
| Original Field Name | New Field Name | Semantic Convention Release |
|
||||
| --------------------------------------- | ----------------------------- | -------------------------------------------------------------------------------------- |
|
||||
| `feature_flag.variant` | `feature_flag.result.variant` | [v1.32.0](https://github.com/open-telemetry/semantic-conventions/releases/tag/v1.32.0) |
|
||||
| `feature_flag.evaluation.reason` | `feature_flag.result.reason` | [v1.32.0](https://github.com/open-telemetry/semantic-conventions/releases/tag/v1.32.0) |
|
||||
| `feature_flag.evaluation.error.message` | `error.message` | [v1.33.0](https://github.com/open-telemetry/semantic-conventions/releases/tag/v1.33.0) |
|
||||
| `feature_flag.provider_name` | `feature_flag.provider.name` | [v1.33.0](https://github.com/open-telemetry/semantic-conventions/releases/tag/v1.33.0) |
|
||||
| `value` | `feature_flag.result.value` | [v1.34.0](https://github.com/open-telemetry/semantic-conventions/releases/tag/v1.34.0) |
|
||||
|
||||
## Footnotes
|
||||
|
||||
[^1]: The `variant` field should be included whenever possible as it represents the symbolic name of the flag's returned value (e.g., "on"/"off", "control"/"treatment"). Only omit if the provider doesn't supply this information.
|
||||
[^2]: The `value` field should be included whenever a `variant` is unavailable. Large or sensitive values should be redacted or omitted prior to being captured in telemetry signals.
|
||||
[^3]: Include `error.type` and `error.message`, if and only if an error occurred during a flag evaluation.
|
||||
|
||||
[otel-ff-logs]: https://opentelemetry.io/docs/specs/semconv/feature-flags/feature-flags-logs/
|
|
@ -0,0 +1,128 @@
|
|||
Feature: Context merging precedence
|
||||
|
||||
Background:
|
||||
Given a stable provider with retrievable context is registered
|
||||
|
||||
Scenario Outline: A context entry is added to a single level
|
||||
Given A context entry with key "key" and value "value" is added to the "<level>" level
|
||||
When Some flag was evaluated
|
||||
Then The merged context contains an entry with key "key" and value "value"
|
||||
|
||||
@transaction
|
||||
Examples:
|
||||
| level |
|
||||
| API |
|
||||
| Transaction |
|
||||
| Client |
|
||||
| Invocation |
|
||||
|
||||
@hooks
|
||||
Examples:
|
||||
| level |
|
||||
| API |
|
||||
| Client |
|
||||
| Invocation |
|
||||
| Before Hooks |
|
||||
|
||||
@hooks @transaction
|
||||
Examples:
|
||||
| level |
|
||||
| API |
|
||||
| Transaction |
|
||||
| Client |
|
||||
| Invocation |
|
||||
| Before Hooks |
|
||||
|
||||
@transaction
|
||||
Scenario: For a transaction, a context entry is added to each level with different keys
|
||||
Given A context entry with key "API" and value "API value" is added to the "API" level
|
||||
And A context entry with key "Transaction" and value "Transaction value" is added to the "Transaction" level
|
||||
And A context entry with key "Client" and value "Client value" is added to the "Client" level
|
||||
And A context entry with key "Invocation" and value "Invocation value" is added to the "Invocation" level
|
||||
When Some flag was evaluated
|
||||
Then The merged context contains an entry with key "API" and value "API value"
|
||||
And The merged context contains an entry with key "Transaction" and value "Transaction value"
|
||||
And The merged context contains an entry with key "Client" and value "Client value"
|
||||
And The merged context contains an entry with key "Invocation" and value "Invocation value"
|
||||
|
||||
@hooks
|
||||
Scenario: For a hook, a context entry is added to each level with different keys
|
||||
Given A context entry with key "API" and value "API value" is added to the "API" level
|
||||
And A context entry with key "Client" and value "Client value" is added to the "Client" level
|
||||
And A context entry with key "Invocation" and value "Invocation value" is added to the "Invocation" level
|
||||
And A context entry with key "Before Hooks" and value "Before Hooks value" is added to the "Before Hooks" level
|
||||
When Some flag was evaluated
|
||||
Then The merged context contains an entry with key "API" and value "API value"
|
||||
And The merged context contains an entry with key "Client" and value "Client value"
|
||||
And The merged context contains an entry with key "Invocation" and value "Invocation value"
|
||||
And The merged context contains an entry with key "Before Hooks" and value "Before Hooks value"
|
||||
|
||||
@hooks @transaction
|
||||
Scenario: For a transaction and a hook, a context entry is added to each level with different keys
|
||||
Given A context entry with key "API" and value "API value" is added to the "API" level
|
||||
And A context entry with key "Transaction" and value "Transaction value" is added to the "Transaction" level
|
||||
And A context entry with key "Client" and value "Client value" is added to the "Client" level
|
||||
And A context entry with key "Invocation" and value "Invocation value" is added to the "Invocation" level
|
||||
And A context entry with key "Before Hooks" and value "Before Hooks value" is added to the "Before Hooks" level
|
||||
When Some flag was evaluated
|
||||
Then The merged context contains an entry with key "API" and value "API value"
|
||||
And The merged context contains an entry with key "Transaction" and value "Transaction value"
|
||||
And The merged context contains an entry with key "Client" and value "Client value"
|
||||
And The merged context contains an entry with key "Invocation" and value "Invocation value"
|
||||
And The merged context contains an entry with key "Before Hooks" and value "Before Hooks value"
|
||||
|
||||
@transaction
|
||||
Scenario Outline: For a transaction, a context entry in one level overwrites values with the same key from preceding levels
|
||||
Given A table with levels of increasing precedence
|
||||
| API |
|
||||
| Transaction |
|
||||
| Client |
|
||||
| Invocation |
|
||||
And Context entries for each level from API level down to the "<level>" level, with key "key" and value "<level>"
|
||||
When Some flag was evaluated
|
||||
Then The merged context contains an entry with key "key" and value "<level>"
|
||||
|
||||
Examples:
|
||||
| level |
|
||||
| API |
|
||||
| Transaction |
|
||||
| Client |
|
||||
| Invocation |
|
||||
|
||||
@hooks
|
||||
Scenario Outline: For a hook, a context entry in one level overwrites values with the same key from preceding levels
|
||||
Given A table with levels of increasing precedence
|
||||
| API |
|
||||
| Client |
|
||||
| Invocation |
|
||||
| Before Hooks |
|
||||
And Context entries for each level from API level down to the "<level>" level, with key "key" and value "<level>"
|
||||
When Some flag was evaluated
|
||||
Then The merged context contains an entry with key "key" and value "<level>"
|
||||
|
||||
Examples:
|
||||
| level |
|
||||
| API |
|
||||
| Client |
|
||||
| Invocation |
|
||||
| Before Hooks |
|
||||
|
||||
@hooks @transaction
|
||||
Scenario Outline: For a transaction and a hook, context entry in one level overwrites values with the same key from preceding levels
|
||||
Given A table with levels of increasing precedence
|
||||
| API |
|
||||
| Transaction |
|
||||
| Client |
|
||||
| Invocation |
|
||||
| Before Hooks |
|
||||
And Context entries for each level from API level down to the "<level>" level, with key "key" and value "<level>"
|
||||
When Some flag was evaluated
|
||||
Then The merged context contains an entry with key "key" and value "<level>"
|
||||
|
||||
Examples:
|
||||
| level |
|
||||
| API |
|
||||
| Transaction |
|
||||
| Client |
|
||||
| Invocation |
|
||||
| Before Hooks |
|
|
@ -0,0 +1,67 @@
|
|||
Feature: Flag evaluation
|
||||
|
||||
# This test suite contains scenarios to test the flag evaluation API.
|
||||
|
||||
Background:
|
||||
Given a stable provider
|
||||
|
||||
# basic evaluation
|
||||
Scenario: Resolves boolean value
|
||||
When a boolean flag with key "boolean-flag" is evaluated with default value "false"
|
||||
Then the resolved boolean value should be "true"
|
||||
|
||||
Scenario: Resolves string value
|
||||
When a string flag with key "string-flag" is evaluated with default value "bye"
|
||||
Then the resolved string value should be "hi"
|
||||
|
||||
Scenario: Resolves integer value
|
||||
When an integer flag with key "integer-flag" is evaluated with default value 1
|
||||
Then the resolved integer value should be 10
|
||||
|
||||
Scenario: Resolves float value
|
||||
When a float flag with key "float-flag" is evaluated with default value 0.1
|
||||
Then the resolved float value should be 0.5
|
||||
|
||||
Scenario: Resolves object value
|
||||
When an object flag with key "object-flag" is evaluated with a null default value
|
||||
Then the resolved object value should be contain fields "showImages", "title", and "imagesPerPage", with values "true", "Check out these pics!" and 100, respectively
|
||||
|
||||
# detailed evaluation
|
||||
Scenario: Resolves boolean details
|
||||
When a boolean flag with key "boolean-flag" is evaluated with details and default value "false"
|
||||
Then the resolved boolean details value should be "true", the variant should be "on", and the reason should be "STATIC"
|
||||
|
||||
Scenario: Resolves string details
|
||||
When a string flag with key "string-flag" is evaluated with details and default value "bye"
|
||||
Then the resolved string details value should be "hi", the variant should be "greeting", and the reason should be "STATIC"
|
||||
|
||||
Scenario: Resolves integer details
|
||||
When an integer flag with key "integer-flag" is evaluated with details and default value 1
|
||||
Then the resolved integer details value should be 10, the variant should be "ten", and the reason should be "STATIC"
|
||||
|
||||
Scenario: Resolves float details
|
||||
When a float flag with key "float-flag" is evaluated with details and default value 0.1
|
||||
Then the resolved float details value should be 0.5, the variant should be "half", and the reason should be "STATIC"
|
||||
|
||||
Scenario: Resolves object details
|
||||
When an object flag with key "object-flag" is evaluated with details and a null default value
|
||||
Then the resolved object details value should be contain fields "showImages", "title", and "imagesPerPage", with values "true", "Check out these pics!" and 100, respectively
|
||||
And the variant should be "template", and the reason should be "STATIC"
|
||||
|
||||
# context-aware evaluation
|
||||
Scenario: Resolves based on context
|
||||
When context contains keys "fn", "ln", "age", "customer" with values "Sulisław", "Świętopełk", 29, "false"
|
||||
And a flag with key "context-aware" is evaluated with default value "EXTERNAL"
|
||||
Then the resolved string response should be "INTERNAL"
|
||||
And the resolved flag value is "EXTERNAL" when the context is empty
|
||||
|
||||
# errors
|
||||
Scenario: Flag not found
|
||||
When a non-existent string flag with key "missing-flag" is evaluated with details and a default value "uh-oh"
|
||||
Then the default string value should be returned
|
||||
And the reason should indicate an error and the error code should indicate a missing flag with "FLAG_NOT_FOUND"
|
||||
|
||||
Scenario: Type error
|
||||
When a string flag with key "wrong-flag" is evaluated as an integer, with details and a default value 13
|
||||
Then the default integer value should be returned
|
||||
And the reason should indicate an error and the error code should indicate a type mismatch with "TYPE_MISMATCH"
|
|
@ -0,0 +1,49 @@
|
|||
@hooks
|
||||
Feature: Evaluation details through hooks
|
||||
|
||||
# This test suite contains scenarios to test the functionality of hooks.
|
||||
|
||||
Background:
|
||||
Given a stable provider
|
||||
|
||||
Scenario: Passes evaluation details to after and finally hooks
|
||||
Given a client with added hook
|
||||
And a boolean-flag with key "boolean-flag" and a default value "false"
|
||||
When the flag was evaluated with details
|
||||
Then the "before" hook should have been executed
|
||||
And the "after, finally" hooks should be called with evaluation details
|
||||
| data_type | key | value |
|
||||
| string | flag_key | boolean-flag |
|
||||
| boolean | value | true |
|
||||
| string | variant | on |
|
||||
| string | reason | STATIC |
|
||||
| string | error_code | null |
|
||||
|
||||
# errors
|
||||
Scenario: Flag not found
|
||||
Given a client with added hook
|
||||
And a string-flag with key "missing-flag" and a default value "uh-oh"
|
||||
When the flag was evaluated with details
|
||||
Then the "before" hook should have been executed
|
||||
And the "error" hook should have been executed
|
||||
And the "finally" hooks should be called with evaluation details
|
||||
| data_type | key | value |
|
||||
| string | flag_key | missing-flag |
|
||||
| string | value | uh-oh |
|
||||
| string | variant | null |
|
||||
| string | reason | ERROR |
|
||||
| string | error_code | FLAG_NOT_FOUND |
|
||||
|
||||
Scenario: Type error
|
||||
Given a client with added hook
|
||||
And a boolean-flag with key "wrong-flag" and a default value "false"
|
||||
When the flag was evaluated with details
|
||||
Then the "before" hook should have been executed
|
||||
And the "error" hook should have been executed
|
||||
And the "finally" hooks should be called with evaluation details
|
||||
| data_type | key | value |
|
||||
| string | flag_key | wrong-flag |
|
||||
| boolean | value | false |
|
||||
| string | variant | null |
|
||||
| string | reason | ERROR |
|
||||
| string | error_code | TYPE_MISMATCH |
|
|
@ -0,0 +1,27 @@
|
|||
@Metadata
|
||||
Feature: Metadata
|
||||
|
||||
Background:
|
||||
Given a stable provider
|
||||
|
||||
Scenario: Returns metadata
|
||||
Given a Boolean-flag with key "metadata-flag" and a default value "true"
|
||||
When the flag was evaluated with details
|
||||
Then the resolved metadata should contain
|
||||
| key | metadata_type | value |
|
||||
| string | String | 1.0.2 |
|
||||
| integer | Integer | 2 |
|
||||
| float | Float | 0.1 |
|
||||
| boolean | Boolean | true |
|
||||
|
||||
Scenario Outline: Returns no metadata
|
||||
Given a <flag_type>-flag with key "<key>" and a default value "<default_value>"
|
||||
When the flag was evaluated with details
|
||||
Then the resolved metadata is empty
|
||||
|
||||
Examples: Flags
|
||||
| key | flag_type | default_value |
|
||||
| boolean-flag | Boolean | true |
|
||||
| integer-flag | Integer | 23 |
|
||||
| float-flag | Float | 2.3 |
|
||||
| string-flag | String | value |
|
|
@ -1,613 +0,0 @@
|
|||
{
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "https://excalidraw.com",
|
||||
"elements": [
|
||||
{
|
||||
"type": "text",
|
||||
"version": 441,
|
||||
"versionNonce": 630753906,
|
||||
"isDeleted": false,
|
||||
"id": "fWR53sO5RGQE_NukjZPMN",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "dashed",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 890,
|
||||
"y": 1000,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 260,
|
||||
"height": 25,
|
||||
"seed": 1929169548,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElements": [],
|
||||
"updated": 1657721892932,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 20,
|
||||
"fontFamily": 1,
|
||||
"text": "Flag Evaluation Life Cycle",
|
||||
"baseline": 18,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top",
|
||||
"containerId": null,
|
||||
"originalText": "Flag Evaluation Life Cycle"
|
||||
},
|
||||
{
|
||||
"type": "arrow",
|
||||
"version": 209,
|
||||
"versionNonce": 98635182,
|
||||
"isDeleted": false,
|
||||
"id": "comxMv7YbRFpt1dbYHSw9",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 756.7414572451423,
|
||||
"y": 849,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 0.7414572451423282,
|
||||
"height": 132,
|
||||
"seed": 2005717004,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"boundElements": [],
|
||||
"updated": 1657721892932,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"startBinding": {
|
||||
"elementId": "0SyqzRLd6i14ANI4cp-V3",
|
||||
"focus": 0.010928467393289114,
|
||||
"gap": 3.5
|
||||
},
|
||||
"endBinding": null,
|
||||
"lastCommittedPoint": null,
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
-0.7414572451423282,
|
||||
-132
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "arrow",
|
||||
"version": 285,
|
||||
"versionNonce": 247622706,
|
||||
"isDeleted": false,
|
||||
"id": "f6SHZ953dRbLcslg-oHpV",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "dashed",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 428,
|
||||
"y": 700,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 1188,
|
||||
"height": 136,
|
||||
"seed": 482789300,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"boundElements": [],
|
||||
"updated": 1657721892932,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"startBinding": null,
|
||||
"endBinding": null,
|
||||
"lastCommittedPoint": null,
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
328,
|
||||
0
|
||||
],
|
||||
[
|
||||
592,
|
||||
-126
|
||||
],
|
||||
[
|
||||
840,
|
||||
4
|
||||
],
|
||||
[
|
||||
1188,
|
||||
10
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "arrow",
|
||||
"version": 170,
|
||||
"versionNonce": 101656558,
|
||||
"isDeleted": false,
|
||||
"id": "hOQcNNx7KUdARzsDabH3G",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "dotted",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 764,
|
||||
"y": 709,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 488,
|
||||
"height": 122,
|
||||
"seed": 1987572492,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"boundElements": [],
|
||||
"updated": 1657721892932,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"startBinding": null,
|
||||
"endBinding": null,
|
||||
"lastCommittedPoint": null,
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
236,
|
||||
120
|
||||
],
|
||||
[
|
||||
488,
|
||||
-2
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"version": 480,
|
||||
"versionNonce": 63575538,
|
||||
"isDeleted": false,
|
||||
"id": "0SyqzRLd6i14ANI4cp-V3",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "dashed",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 680,
|
||||
"y": 852.5,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 152,
|
||||
"height": 25,
|
||||
"seed": 1635894540,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElements": [
|
||||
{
|
||||
"id": "comxMv7YbRFpt1dbYHSw9",
|
||||
"type": "arrow"
|
||||
}
|
||||
],
|
||||
"updated": 1657721892932,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 20,
|
||||
"fontFamily": 1,
|
||||
"text": "Flag evaluation",
|
||||
"baseline": 18,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top",
|
||||
"containerId": null,
|
||||
"originalText": "Flag evaluation"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"version": 659,
|
||||
"versionNonce": 100661806,
|
||||
"isDeleted": false,
|
||||
"id": "DeLSf4xf9PqFaF3jbukNq",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "dashed",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 510,
|
||||
"y": 582.25,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 146,
|
||||
"height": 25,
|
||||
"seed": 1876044300,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElements": [
|
||||
{
|
||||
"id": "F3WMEgn3efPIVfvbnc7Ip",
|
||||
"type": "arrow"
|
||||
}
|
||||
],
|
||||
"updated": 1657721892932,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 20,
|
||||
"fontFamily": 1,
|
||||
"text": "\"Before\" stage",
|
||||
"baseline": 18,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top",
|
||||
"containerId": null,
|
||||
"originalText": "\"Before\" stage"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"version": 60,
|
||||
"versionNonce": 1438025650,
|
||||
"isDeleted": false,
|
||||
"id": "yEi8T8PClIvN-AkBfLMYy",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "dashed",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 940,
|
||||
"y": 926,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 131,
|
||||
"height": 25,
|
||||
"seed": 397093556,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElements": [
|
||||
{
|
||||
"id": "rldjo9Bs_0OE8EKg7WDAB",
|
||||
"type": "arrow"
|
||||
}
|
||||
],
|
||||
"updated": 1657721892932,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 20,
|
||||
"fontFamily": 1,
|
||||
"text": "\"Error\" stage",
|
||||
"baseline": 18,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top",
|
||||
"containerId": null,
|
||||
"originalText": "\"Error\" stage"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"version": 91,
|
||||
"versionNonce": 1822196846,
|
||||
"isDeleted": false,
|
||||
"id": "5wDUlPd8B_jixs5NBT90L",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "dashed",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 942,
|
||||
"y": 450,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 134,
|
||||
"height": 25,
|
||||
"seed": 1807170996,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElements": [
|
||||
{
|
||||
"id": "mApSEU77V3z0-ar-pavXA",
|
||||
"type": "arrow"
|
||||
}
|
||||
],
|
||||
"updated": 1657721892932,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 20,
|
||||
"fontFamily": 1,
|
||||
"text": "\"After\" stage",
|
||||
"baseline": 18,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top",
|
||||
"containerId": null,
|
||||
"originalText": "\"After\" stage"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"version": 64,
|
||||
"versionNonce": 481214834,
|
||||
"isDeleted": false,
|
||||
"id": "ODtqRP1bT-9qVF3QdR4Zz",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "dashed",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 1374,
|
||||
"y": 590,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 139,
|
||||
"height": 25,
|
||||
"seed": 1331492620,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElements": [
|
||||
{
|
||||
"id": "JLdSBG-06gcICnQFbizDD",
|
||||
"type": "arrow"
|
||||
}
|
||||
],
|
||||
"updated": 1657721892932,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 20,
|
||||
"fontFamily": 1,
|
||||
"text": "\"Finally\" stage",
|
||||
"baseline": 18,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top",
|
||||
"containerId": null,
|
||||
"originalText": "\"Finally\" stage"
|
||||
},
|
||||
{
|
||||
"type": "arrow",
|
||||
"version": 58,
|
||||
"versionNonce": 1538115246,
|
||||
"isDeleted": false,
|
||||
"id": "JLdSBG-06gcICnQFbizDD",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 1444,
|
||||
"y": 627,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 0,
|
||||
"height": 82,
|
||||
"seed": 1045076748,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"boundElements": [],
|
||||
"updated": 1657721892932,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"startBinding": {
|
||||
"elementId": "ODtqRP1bT-9qVF3QdR4Zz",
|
||||
"focus": -0.007194244604316547,
|
||||
"gap": 12
|
||||
},
|
||||
"endBinding": null,
|
||||
"lastCommittedPoint": null,
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
0,
|
||||
82
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "arrow",
|
||||
"version": 54,
|
||||
"versionNonce": 1408877362,
|
||||
"isDeleted": false,
|
||||
"id": "mApSEU77V3z0-ar-pavXA",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 1016,
|
||||
"y": 483,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 2,
|
||||
"height": 80,
|
||||
"seed": 1589915700,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"boundElements": [],
|
||||
"updated": 1657721892932,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"startBinding": {
|
||||
"elementId": "5wDUlPd8B_jixs5NBT90L",
|
||||
"focus": -0.09637883008356547,
|
||||
"gap": 8
|
||||
},
|
||||
"endBinding": null,
|
||||
"lastCommittedPoint": null,
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
2,
|
||||
80
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "arrow",
|
||||
"version": 51,
|
||||
"versionNonce": 1406976238,
|
||||
"isDeleted": false,
|
||||
"id": "rldjo9Bs_0OE8EKg7WDAB",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 1008,
|
||||
"y": 915,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 2,
|
||||
"height": 78,
|
||||
"seed": 1974872460,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"boundElements": [],
|
||||
"updated": 1657721892932,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"startBinding": {
|
||||
"elementId": "yEi8T8PClIvN-AkBfLMYy",
|
||||
"focus": 0.047136735488897546,
|
||||
"gap": 11
|
||||
},
|
||||
"endBinding": null,
|
||||
"lastCommittedPoint": null,
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
-2,
|
||||
-78
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "arrow",
|
||||
"version": 61,
|
||||
"versionNonce": 1979616498,
|
||||
"isDeleted": false,
|
||||
"id": "F3WMEgn3efPIVfvbnc7Ip",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 584,
|
||||
"y": 623,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 0,
|
||||
"height": 78,
|
||||
"seed": 939203124,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"boundElements": [],
|
||||
"updated": 1657721892932,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"startBinding": {
|
||||
"elementId": "DeLSf4xf9PqFaF3jbukNq",
|
||||
"focus": -0.0136986301369863,
|
||||
"gap": 15.75
|
||||
},
|
||||
"endBinding": null,
|
||||
"lastCommittedPoint": null,
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
0,
|
||||
78
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "NkDXWchoeeUIU--BNVMhT",
|
||||
"type": "line",
|
||||
"x": 581.5249175743113,
|
||||
"y": 716.6987736687283,
|
||||
"width": 410,
|
||||
"height": 114,
|
||||
"angle": 0,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "dotted",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"seed": 407117490,
|
||||
"version": 212,
|
||||
"versionNonce": 140528046,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1657721903964,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
106,
|
||||
60
|
||||
],
|
||||
[
|
||||
276,
|
||||
112
|
||||
],
|
||||
[
|
||||
410,
|
||||
114
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": null,
|
||||
"endBinding": null,
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": null
|
||||
}
|
||||
],
|
||||
"appState": {
|
||||
"gridSize": null,
|
||||
"viewBackgroundColor": "#ffffff"
|
||||
},
|
||||
"files": {}
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 52 KiB |
|
@ -1,431 +0,0 @@
|
|||
{
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "https://excalidraw.com",
|
||||
"elements": [
|
||||
{
|
||||
"id": "sFzbmoq8x0HyykU9HhKbf",
|
||||
"type": "rectangle",
|
||||
"x": 946,
|
||||
"y": 235,
|
||||
"width": 40,
|
||||
"height": 360,
|
||||
"angle": 0,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"seed": 97747724,
|
||||
"version": 143,
|
||||
"versionNonce": 1741772596,
|
||||
"isDeleted": false,
|
||||
"boundElements": [
|
||||
{
|
||||
"id": "GjpO8NH8C2w3d6tgZEw0a",
|
||||
"type": "arrow"
|
||||
},
|
||||
{
|
||||
"id": "YNZgdJRPYhxEAG9UTiiaX",
|
||||
"type": "arrow"
|
||||
}
|
||||
],
|
||||
"updated": 1656091357618,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "AyfB6rhs9QP8dDml-jTtM",
|
||||
"type": "diamond",
|
||||
"x": 1104,
|
||||
"y": 301,
|
||||
"width": 230,
|
||||
"height": 218,
|
||||
"angle": 0,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"seed": 1169897996,
|
||||
"version": 109,
|
||||
"versionNonce": 1736986804,
|
||||
"isDeleted": false,
|
||||
"boundElements": [
|
||||
{
|
||||
"id": "2X5Ue2aHSLNyoaJiVed-6",
|
||||
"type": "arrow"
|
||||
},
|
||||
{
|
||||
"id": "GjpO8NH8C2w3d6tgZEw0a",
|
||||
"type": "arrow"
|
||||
}
|
||||
],
|
||||
"updated": 1656091357618,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "ACCE9Pv-d-jAR2g3-eEcn",
|
||||
"type": "text",
|
||||
"x": 890,
|
||||
"y": 400,
|
||||
"width": 152,
|
||||
"height": 25,
|
||||
"angle": 1.55517259817442,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"seed": 398017588,
|
||||
"version": 157,
|
||||
"versionNonce": 1160434484,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1656092204660,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "Evaluation API",
|
||||
"fontSize": 20,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top",
|
||||
"baseline": 18,
|
||||
"containerId": null,
|
||||
"originalText": "Evaluation API"
|
||||
},
|
||||
{
|
||||
"id": "XTsFx3AIH7HTI_Sd1EtQQ",
|
||||
"type": "text",
|
||||
"x": 1180,
|
||||
"y": 398,
|
||||
"width": 80,
|
||||
"height": 25,
|
||||
"angle": 0,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"seed": 468363700,
|
||||
"version": 111,
|
||||
"versionNonce": 72997556,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1656092173540,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "Provider",
|
||||
"fontSize": 20,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top",
|
||||
"baseline": 18,
|
||||
"containerId": null,
|
||||
"originalText": "Provider"
|
||||
},
|
||||
{
|
||||
"id": "hk-L4sS1BEfbVBOgFS7g1",
|
||||
"type": "text",
|
||||
"x": 1478,
|
||||
"y": 390,
|
||||
"width": 164,
|
||||
"height": 50,
|
||||
"angle": 0,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"seed": 735367860,
|
||||
"version": 227,
|
||||
"versionNonce": 523408436,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1656092191304,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "Flag management\n system",
|
||||
"fontSize": 20,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "top",
|
||||
"baseline": 43,
|
||||
"containerId": null,
|
||||
"originalText": "Flag management\n system"
|
||||
},
|
||||
{
|
||||
"id": "GjpO8NH8C2w3d6tgZEw0a",
|
||||
"type": "arrow",
|
||||
"x": 994,
|
||||
"y": 413,
|
||||
"width": 100,
|
||||
"height": 2,
|
||||
"angle": 0,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"seed": 2119779508,
|
||||
"version": 144,
|
||||
"versionNonce": 1893866804,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1656091357618,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
100,
|
||||
-2
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "sFzbmoq8x0HyykU9HhKbf",
|
||||
"focus": -0.007982261640798228,
|
||||
"gap": 8
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "AyfB6rhs9QP8dDml-jTtM",
|
||||
"focus": 0.013761467889908258,
|
||||
"gap": 7.6049836494533025
|
||||
},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow"
|
||||
},
|
||||
{
|
||||
"type": "arrow",
|
||||
"version": 247,
|
||||
"versionNonce": 1119338804,
|
||||
"isDeleted": false,
|
||||
"id": "2X5Ue2aHSLNyoaJiVed-6",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 1342,
|
||||
"y": 412,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 100,
|
||||
"height": 2,
|
||||
"seed": 1010136244,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"boundElements": [],
|
||||
"updated": 1656091362726,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"startBinding": {
|
||||
"elementId": "AyfB6rhs9QP8dDml-jTtM",
|
||||
"focus": 0.04091743119266055,
|
||||
"gap": 6.954931105143189
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "OVJNNaVaqA4rfwXZ4m26c",
|
||||
"focus": 0.043664558253417675,
|
||||
"gap": 13
|
||||
},
|
||||
"lastCommittedPoint": null,
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
100,
|
||||
-2
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"version": 315,
|
||||
"versionNonce": 165520820,
|
||||
"isDeleted": false,
|
||||
"id": "j3MxwuU4vlV0o75SwNm6-",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 681.5,
|
||||
"y": 400.5,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 103,
|
||||
"height": 25,
|
||||
"seed": 1401550644,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElements": [],
|
||||
"updated": 1656092169786,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 20,
|
||||
"fontFamily": 1,
|
||||
"text": "Application",
|
||||
"baseline": 18,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top",
|
||||
"containerId": null,
|
||||
"originalText": "Application"
|
||||
},
|
||||
{
|
||||
"type": "arrow",
|
||||
"version": 202,
|
||||
"versionNonce": 1322315020,
|
||||
"isDeleted": false,
|
||||
"id": "YNZgdJRPYhxEAG9UTiiaX",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 838,
|
||||
"y": 413,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 100,
|
||||
"height": 2,
|
||||
"seed": 873715764,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"boundElements": [],
|
||||
"updated": 1656091357618,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"startBinding": {
|
||||
"elementId": "FkWpwmnzGSQRmo6WPzc-b",
|
||||
"focus": 0.01163027953478882,
|
||||
"gap": 6
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "sFzbmoq8x0HyykU9HhKbf",
|
||||
"focus": 0.025277161862527722,
|
||||
"gap": 8
|
||||
},
|
||||
"lastCommittedPoint": null,
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
100,
|
||||
-2
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "FkWpwmnzGSQRmo6WPzc-b",
|
||||
"type": "rectangle",
|
||||
"x": 630,
|
||||
"y": 318,
|
||||
"width": 202,
|
||||
"height": 192,
|
||||
"angle": 0,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"seed": 694797236,
|
||||
"version": 157,
|
||||
"versionNonce": 46623628,
|
||||
"isDeleted": false,
|
||||
"boundElements": [
|
||||
{
|
||||
"id": "YNZgdJRPYhxEAG9UTiiaX",
|
||||
"type": "arrow"
|
||||
}
|
||||
],
|
||||
"updated": 1656091357619,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"type": "rectangle",
|
||||
"version": 234,
|
||||
"versionNonce": 914824204,
|
||||
"isDeleted": false,
|
||||
"id": "OVJNNaVaqA4rfwXZ4m26c",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 1455,
|
||||
"y": 316,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 202,
|
||||
"height": 192,
|
||||
"seed": 1478245516,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElements": [
|
||||
{
|
||||
"id": "YNZgdJRPYhxEAG9UTiiaX",
|
||||
"type": "arrow"
|
||||
},
|
||||
{
|
||||
"id": "2X5Ue2aHSLNyoaJiVed-6",
|
||||
"type": "arrow"
|
||||
}
|
||||
],
|
||||
"updated": 1656091362726,
|
||||
"link": null,
|
||||
"locked": false
|
||||
}
|
||||
],
|
||||
"appState": {
|
||||
"gridSize": null,
|
||||
"viewBackgroundColor": "#ffffff"
|
||||
},
|
||||
"files": {}
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 49 KiB |
|
@ -1,27 +0,0 @@
|
|||
# Evaluation Context
|
||||
|
||||
**Status**: [Experimental](./README.md#document-statuses)
|
||||
|
||||
## Overview
|
||||
|
||||
The `evaluation context` provides ambient information for the purposes of flag evaluation. Contextual data may be used as the basis for targeting, including rule-based evaluation, overrides for specific subjects, or fractional flag evaluation.
|
||||
|
||||
The context might contain information about the end-user, the application, the host, or any other ambient data that might be useful in flag evaluation. For example, a flag system might define rules that return a specific value based on the user's email address, locale, or the time of day. The context provides this information. The context can be optionally provided at evaluation, and mutated in [before hooks](./hooks.md).
|
||||
|
||||
### Fields
|
||||
|
||||
NOTE: Field casing is not specified, and should be chosen in accordance with language idioms.
|
||||
|
||||
see: [types](./types.md)
|
||||
|
||||
#### Requirement 3.1
|
||||
|
||||
> The `evaluation context` structure **MUST** define an optional `targeting key` field of type string, identifying the subject of the flag evaluation.
|
||||
|
||||
The targeting key uniquely identifies the subject (end-user, or client service) of a flag evaluation. Providers may require this field for fractional flag evaluation, rules, or overrides targeting specific users. Such providers may behave unpredictably if a targeting key is not specified at flag resolution.
|
||||
|
||||
#### Requirement 3.2
|
||||
|
||||
> The evaluation context **MUST** support the inclusion of custom fields, having keys of type `string`, and values of type `boolean | string | number | datetime | structure`.
|
||||
|
||||
see: [structure](./types.md#structure), [datetime](./types.md#datetime)
|
|
@ -1,218 +0,0 @@
|
|||
# Flag Evaluation API
|
||||
|
||||
**Status**: [Experimental](./README.md#document-statuses)
|
||||
|
||||
## Overview
|
||||
|
||||
The `evaluation API` allows for the evaluation of feature flag values, independent of any flag control plane or vendor. In the absence of a [provider](./providers.md) the `evaluation API` uses the "No-op provider", which simply returns the supplied default flag value.
|
||||
|
||||
### API Initialization and Configuration
|
||||
|
||||
#### Requirement 1.1.1
|
||||
|
||||
> The `API`, and any state it maintains **SHOULD** exist as a global singleton, even in cases wherein multiple versions of the `API` are present at runtime.
|
||||
|
||||
It's important that multiple instances of the `API` not be active, so that state stored therein, such as the registered `provider`, static global `evaluation context`, and globally configured `hooks` allow the `API` to behave predictably. This can be difficult in some runtimes or languages, but implementors should make their best effort to ensure that only a single instance of the `API` is used.
|
||||
|
||||
#### Requirement 1.1.2
|
||||
|
||||
> The `API` **MUST** provide a function to set the global `provider` singleton, which accepts an API-conformant `provider` implementation.
|
||||
|
||||
```typescript
|
||||
// example provider mutator
|
||||
OpenFeature.setProvider(new MyProvider());
|
||||
```
|
||||
|
||||
See [provider](./providers.md) for details.
|
||||
|
||||
#### Requirement 1.1.3
|
||||
|
||||
> The `API` **MUST** provide a function to add `hooks` which accepts one or more API-conformant `hooks`, and appends them to the collection of any previously added hooks. When new hooks are added, previously added hooks are not removed.
|
||||
|
||||
```typescript
|
||||
// example hook attachment
|
||||
OpenFeature.addHooks([new MyHook()]);
|
||||
```
|
||||
|
||||
See [hooks](./hooks.md) for details.
|
||||
|
||||
#### Requirement 1.1.4
|
||||
|
||||
> The API **MUST** provide a function for retrieving the metadata field of the configured `provider`.
|
||||
|
||||
```typescript
|
||||
// example provider accessor
|
||||
OpenFeature.getProviderMetadata();
|
||||
```
|
||||
|
||||
See [provider](./providers.md) for details.
|
||||
|
||||
#### Requirement 1.1.5
|
||||
|
||||
> The `API` **MUST** provide a function for creating a `client` which accepts the following options:
|
||||
>
|
||||
> - name (optional): A logical string identifier for the client.
|
||||
|
||||
```typescript
|
||||
// example client creation and retrieval
|
||||
OpenFeature.getClient({
|
||||
name: "my-openfeature-client",
|
||||
});
|
||||
```
|
||||
|
||||
The name is a logical identifier for the client.
|
||||
|
||||
#### Requirement 1.1.6
|
||||
|
||||
> The client creation function **MUST NOT** throw, or otherwise abnormally terminate.
|
||||
|
||||
Clients may be created in critical code paths, and even per-request in server-side HTTP contexts. Therefore, in keeping with the principle that OpenFeature should never cause abnormal execution of the first party application, this function should never throw. Abnormal execution in initialization should instead occur during provider registration.
|
||||
|
||||
### Client Usage
|
||||
|
||||
#### Requirement 1.2.1
|
||||
|
||||
> The client **MUST** provide a method to add `hooks` which accepts one or more API-conformant `hooks`, and appends them to the collection of any previously added hooks. When new hooks are added, previously added hooks are not removed.
|
||||
|
||||
```typescript
|
||||
// example hook attachment
|
||||
client.addHooks([new MyHook()]);
|
||||
```
|
||||
|
||||
See [hooks](./hooks.md) for details.
|
||||
|
||||
#### Requirement 1.2.2
|
||||
|
||||
> The client interface **MUST** define a `metadata` member or accessor, containing an immutable `name` field or accessor of type string, which corresponds to the `name` value supplied during client creation.
|
||||
|
||||
```typescript
|
||||
client.getMetadata().getName(); // "my-client"
|
||||
```
|
||||
|
||||
#### Flag Evaluation
|
||||
|
||||
##### Requirement 1.3.1
|
||||
|
||||
> The `client` **MUST** provide methods for flag evaluation, with parameters `flag key` (string, required), `default value` (boolean | number | string | structure, required), `evaluation context` (optional), and `evaluation options` (optional), which returns the flag value.
|
||||
|
||||
```typescript
|
||||
// example flag evaluation
|
||||
var myValue = client.getValue("my-flag", false);
|
||||
```
|
||||
|
||||
##### Condition 1.3.2
|
||||
|
||||
> The language type system differentiates between strings, numbers, booleans and structures.
|
||||
|
||||
###### Conditional Requirement 1.3.2.1
|
||||
|
||||
> The `client` **MUST** provide methods for typed flag evaluation, including boolean, numeric, string, and structure.
|
||||
|
||||
```typescript
|
||||
// example boolean flag evaluation
|
||||
boolean myBool = client.getBooleanValue('bool-flag', false);
|
||||
|
||||
// example overloaded string flag evaluation with optional params
|
||||
string myString = client.getStringValue('string-flag', 'N/A', evaluationContext, options);
|
||||
|
||||
// example number flag evaluation
|
||||
number myNumber = client.getNumberValue('number-flag', 75);
|
||||
|
||||
// example overloaded structure flag evaluation with optional params
|
||||
MyStruct myStruct = client.getObjectValue<MyStruct>('structured-flag', { text: 'N/A', percentage: 75 }, evaluationContext, options);
|
||||
```
|
||||
|
||||
See [evaluation context](./evaluation-context.md) for details.
|
||||
|
||||
###### Conditional Requirement 1.3.3
|
||||
|
||||
> The `client` **SHOULD** guarantee the returned value of any typed flag evaluation method is of the expected type. If the value returned by the underlying provider implementation does not match the expected type, it's to be considered abnormal execution, and the supplied `default value` should be returned.
|
||||
|
||||
#### Detailed Flag Evaluation
|
||||
|
||||
##### Requirement 1.4.1
|
||||
|
||||
> The `client` **MUST** provide methods for detailed flag value evaluation with parameters `flag key` (string, required), `default value` (boolean | number | string | structure, required), `evaluation context` (optional), and `evaluation options` (optional), which returns an `evaluation details` structure.
|
||||
|
||||
```typescript
|
||||
// example detailed boolean flag evaluation
|
||||
FlagEvaluationDetails<boolean> myBoolDetails = client.getBooleanDetails('bool-flag', false);
|
||||
|
||||
// example detailed string flag evaluation
|
||||
FlagEvaluationDetails<string> myStringDetails = client.getStringDetails('string-flag', 'N/A', evaluationContext, options);
|
||||
|
||||
// example detailed number flag evaluation
|
||||
FlagEvaluationDetails<number> myNumberDetails = client.getNumberDetails('number-flag', 75);
|
||||
|
||||
// example detailed structure flag evaluation
|
||||
FlagEvaluationDetails<MyStruct> myStructDetails = client.getObjectDetails<MyStruct>('structured-flag', { text: 'N/A', percentage: 75 }, evaluationContext, options);
|
||||
|
||||
```
|
||||
|
||||
##### Requirement 1.4.2
|
||||
|
||||
> The `evaluation details` structure's `value` field **MUST** contain the evaluated flag value.
|
||||
|
||||
##### Condition 1.4.3
|
||||
|
||||
> The language supports generics (or an equivalent feature).
|
||||
|
||||
###### Conditional Requirement 1.4.3.1
|
||||
|
||||
> The `evaluation details` structure **SHOULD** accept a generic argument (or use an equivalent language feature) which indicates the type of the wrapped `value` field.
|
||||
|
||||
##### Requirement 1.4.4
|
||||
|
||||
> The `evaluation details` structure's `flag key` field **MUST** contain the `flag key` argument passed to the detailed flag evaluation method.
|
||||
|
||||
##### Requirement 1.4.5
|
||||
|
||||
> In cases of normal execution, the `evaluation details` structure's `variant` field **MUST** contain the value of the `variant` field in the `flag resolution` structure returned by the configured `provider`, if the field is set.
|
||||
|
||||
##### Requirement 1.4.6
|
||||
|
||||
> In cases of normal execution, the `evaluation details` structure's `reason` field **MUST** contain the value of the `reason` field in the `flag resolution` structure returned by the configured `provider`, if the field is set.
|
||||
|
||||
##### Requirement 1.4.7
|
||||
|
||||
> In cases of abnormal execution, the `evaluation details` structure's `error code` field **MUST** contain a string identifying an error occurred during flag evaluation and the nature of the error.
|
||||
|
||||
Some example error codes include: `"TARGETING_KEY_MISSING"`, `"PROVIDER_NOT_READY"`, `"FLAG_NOT_FOUND"`, `"PARSE_ERROR"`, `"TYPE_MISMATCH"`, or `"GENERAL"`.
|
||||
|
||||
##### Requirement 1.4.8
|
||||
|
||||
> In cases of abnormal execution (network failure, unhandled error, etc) the `reason` field in the `evaluation details` **SHOULD** indicate an error.
|
||||
|
||||
##### Requirement 1.4.9
|
||||
|
||||
> Methods, functions, or operations on the client **MUST NOT** throw exceptions, or otherwise abnormally terminate. Flag evaluation calls must always return the `default value` in the event of abnormal execution. Exceptions include functions or methods for the purposes for configuration or setup.
|
||||
|
||||
Configuration code includes code to set the provider, instantiate providers, and configure the global API object.
|
||||
|
||||
##### Requirement 1.4.10
|
||||
|
||||
> In the case of abnormal execution, the client **SHOULD** log an informative error message.
|
||||
|
||||
Implementations may define a standard logging interface that can be supplied as an optional argument to the client creation function, which may wrap standard logging functionality of the implementation language.
|
||||
|
||||
##### Requirement 1.4.11
|
||||
|
||||
> The `client` **SHOULD** provide asynchronous or non-blocking mechanisms for flag evaluation.
|
||||
|
||||
It's recommended to provide non-blocking mechanisms for flag evaluation, particularly in languages or environments wherein there's a single thread of execution.
|
||||
|
||||
#### Evaluation Options
|
||||
|
||||
##### Requirement 1.5.1
|
||||
|
||||
> The `evaluation options` structure's `hooks` field denotes an ordered collection of hooks that the client **MUST** execute for the respective flag evaluation, in addition to those already configured.
|
||||
|
||||
See [hooks](./hooks.md) for details.
|
||||
|
||||
#### Context Transformation
|
||||
|
||||
##### Requirement 1.6.1
|
||||
|
||||
> The `client` **SHOULD** transform the `evaluation context` using the `provider's` `context transformer` function if one is defined, before passing the result of the transformation to the provider's flag resolution functions.
|
||||
|
||||
See [context transformation](./providers.md#context-transformation) for details.
|
|
@ -1,4 +1,10 @@
|
|||
# Glossary
|
||||
---
|
||||
title: Glossary
|
||||
description: A list of terms used within the OpenFeature specification.
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# Glossary <!-- omit from toc -->
|
||||
|
||||
This document defines some terms that are used across this specification.
|
||||
|
||||
|
@ -16,16 +22,24 @@ This document defines some terms that are used across this specification.
|
|||
- [Library Author](#library-author)
|
||||
- [Common](#common)
|
||||
- [Feature Flag SDK](#feature-flag-sdk)
|
||||
- [Client-Side SDK](#client-side-sdk)
|
||||
- [Server-Side SDK](#server-side-sdk)
|
||||
- [Feature Flag API](#feature-flag-api)
|
||||
- [Evaluation API](#evaluation-api)
|
||||
- [Flag Management System](#flag-management-system)
|
||||
- [Client](#client)
|
||||
- [Provider](#provider)
|
||||
- [Provider Lifecycle](#provider-lifecycle)
|
||||
- [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)
|
||||
- [Tracking Event](#tracking-event)
|
||||
- [Flagging specifics](#flagging-specifics)
|
||||
- [Flag](#flag)
|
||||
- [Flag Set](#flag-set)
|
||||
- [Flag Key](#flag-key)
|
||||
- [Variant](#variant)
|
||||
- [Values](#values)
|
||||
|
@ -33,6 +47,9 @@ This document defines some terms that are used across this specification.
|
|||
- [Targeting Key](#targeting-key)
|
||||
- [Fractional Evaluation](#fractional-evaluation)
|
||||
- [Rule](#rule)
|
||||
- [SDK Paradigms](#sdk-paradigms)
|
||||
- [Dynamic-Context Paradigm](#dynamic-context-paradigm)
|
||||
- [Static-Context Paradigm](#static-context-paradigm)
|
||||
|
||||
<!-- tocstop -->
|
||||
|
||||
|
@ -54,7 +71,7 @@ A developer who is setting up or configuring an application or service to use th
|
|||
|
||||
### Provider Author
|
||||
|
||||
The maintainer of an API-compliant [provider](./providers.md) which implements the necessary interfaces required for flag evaluation.
|
||||
The maintainer of an API-compliant [provider](./sections/02-providers.md) which implements the necessary interfaces required for flag evaluation.
|
||||
|
||||
### Integration Author
|
||||
|
||||
|
@ -68,7 +85,15 @@ The maintainer of a shared library which is a dependency of many applications or
|
|||
|
||||
### Feature Flag SDK
|
||||
|
||||
The libraries used by Application Author to implement feature flags in their application or service. The interfaces defined in these libraries adhere to the Feature Flag API.
|
||||
The libraries used by the Application Author to implement feature flags in their application or service. The interfaces defined in these libraries adhere to the Feature Flag API.
|
||||
|
||||
### Client-Side SDK
|
||||
|
||||
An SDK which is built for usage in client applications (e.g. single-page web applications), and typically uses the [static-context paradigm](#static-context-paradigm).
|
||||
|
||||
### Server-Side SDK
|
||||
|
||||
An SDK which is built for usage in server applications (e.g. REST services), and typically uses the [dynamic-context paradigm](#dynamic-context-paradigm).
|
||||
|
||||
### Feature Flag API
|
||||
|
||||
|
@ -84,9 +109,21 @@ The subset of the [Feature Flag API](#feature-flag-api) that the Application Aut
|
|||
|
||||
A source-of-truth for flag values and rules. Flag management systems may include SaaS feature flag vendors, custom "in-house" feature flag infrastructure, or open-source implementations.
|
||||
|
||||
### Client
|
||||
|
||||
A lightweight abstraction that provides functions to evaluate feature flags. A client is associated with a single provider, which it uses to perform evaluations.
|
||||
|
||||
### Provider
|
||||
|
||||
An SDK-compliant implementation which resolves flag values from a particular flag management system, allowing the use of the [Evaluation API](./flag-evaluation.md#flag-evaluation) as an abstraction for the system in question.
|
||||
An SDK-compliant implementation which resolves flag values from a particular flag management system, allowing the use of the [Evaluation API](./sections/01-flag-evaluation.md#13-flag-evaluation) as an abstraction for the system in question.
|
||||
|
||||
### Provider Lifecycle
|
||||
|
||||
The possible states and transitions of a provider over the course of its usage, as defined by the [provider interface](./sections/02-providers.md).
|
||||
|
||||
### Domain
|
||||
|
||||
An identifier which logically binds clients with providers, allowing for multiple providers to be used simultaneously within a single application. Domain binding is dynamic; it may change over the course of an application's lifetime (i.e.: a client associated with the default provider via an unbound domain will be bound to a new provider if a provider is subsequently assigned to that domain).
|
||||
|
||||
### Integration
|
||||
|
||||
|
@ -96,9 +133,13 @@ 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:
|
||||
The process of retrieving a feature flag value in its entirety, including:
|
||||
|
||||
- any effects resulting from hooks
|
||||
- resolving a flag value from a configured provider
|
||||
|
@ -106,7 +147,11 @@ The process of retrieving a feature flag value in it's entirety, including:
|
|||
|
||||
### Resolving Flag Values
|
||||
|
||||
The process of a provider retrieving a feature flag value from it's particular source-of-truth.
|
||||
The process of a provider retrieving a feature flag value from its particular source-of-truth.
|
||||
|
||||
### Tracking Event
|
||||
|
||||
A particular user action or application state representing a business objective or outcome, identified by a unique string, and recorded using the [tracking API](./sections/06-tracking.md).
|
||||
|
||||
## Flagging specifics
|
||||
|
||||
|
@ -122,9 +167,13 @@ erDiagram
|
|||
|
||||
Flags represent a single pivot point of logic. Flags have a type, like `string`, `boolean`, `json`, etc. Examples: `redesign_enabled` or `header-order`
|
||||
|
||||
### Flag Set
|
||||
|
||||
A collection of related [flags](#flag). This grouping helps organize feature flags based on their intended use, facilitating easier management and deployment.
|
||||
|
||||
### Flag Key
|
||||
|
||||
A string logically identifies a particular flag.
|
||||
A string that logically identifies a particular flag.
|
||||
|
||||
### Variant
|
||||
|
||||
|
@ -150,8 +199,30 @@ A string logically identifying the subject of evaluation (end-user, service, etc
|
|||
|
||||
### Fractional Evaluation
|
||||
|
||||
Assigning flag values randomly according to a configured proportion or percentage (ie: 50/50).
|
||||
Pseudorandomly resolve flag values using a context property, such as a targeting key, based on a configured proportion or percentage (ie: 50/50).
|
||||
|
||||
### Rule
|
||||
|
||||
A rule is some criteria that's used to determine which variant a particular context should be mapped to.
|
||||
|
||||
## SDK Paradigms
|
||||
|
||||
Feature flag frameworks have SDKs which operate in two distinct paradigms: those designed for use with a single user client application (e.g. mobile phones, single-page web apps), and those designed for multi-user applications, such as web server applications. Some parts of the OpenFeature specification diverge depending on the paradigm.
|
||||
|
||||
### Dynamic-Context Paradigm
|
||||
|
||||
Server-side applications typically perform flag evaluations on behalf of many users, with each request or event being associated with a particular user or client. For this reason, server frameworks typically operate similarly to this:
|
||||
|
||||
- the application is initialized with some static context (geography, service name, hostname, etc)
|
||||
- with each request or event, relevant dynamic context (for example, user session data, unique user identifiers) is provided to flag evaluations
|
||||
|
||||
### Static-Context Paradigm
|
||||
|
||||
In contrast to server-side or other service-type applications, client side applications typically operate in the context of a single user. Most feature flagging libraries for these applications have been designed with this in mind. Frequently, client/web libraries operate similarly to this:
|
||||
|
||||
- an initialization occurs, which fetches evaluated flags in bulk for a given context (user)
|
||||
- the evaluated flags are cached in the library
|
||||
- flag evaluations take place against this cache, without a need to provide context (context was already used to evaluate flags in bulk)
|
||||
- libraries provide a mechanism to update context (e.g. if a user logs in), meaning cached evaluations are no longer valid and must be re-evaluated, frequently involving a network request or I/O operation
|
||||
|
||||
Not all client libraries work this way, but generally, libraries that accept dynamic context per evaluation can build providers which conform to this model with relative ease, while the reverse is not true.
|
||||
|
|
|
@ -1,183 +0,0 @@
|
|||
# Hooks
|
||||
|
||||
## Overview
|
||||
|
||||
`Hooks` are a mechanism whereby application developers can add arbitrary behavior to flag evaluation. They operate similarly to middleware in many web frameworks.
|
||||
|
||||
Hooks add their logic at any of four specific stages of flag evaluation:
|
||||
|
||||
- `before`, immediately before flag evaluation
|
||||
- `after`, immediately after successful flag evaluation
|
||||
- `error`, immediately after an unsuccessful during flag evaluation
|
||||
- `finally` unconditionally after flag evaluation
|
||||
|
||||

|
||||
|
||||
Hooks can be configured to run globally (impacting all flag evaluations), per client, or per flag evaluation invocation. Some example use-cases for hook include adding additional data to the [evaluation context](./evaluation-context.md), performing validation on the received flag value, providing data to telemetric tools, and logging errors.
|
||||
|
||||
### 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 context
|
||||
|
||||
Hook context exists to provide hooks with information about the invocation.
|
||||
|
||||
#### Requirement 4.1.1
|
||||
|
||||
> Hook context **MUST** provide: the `flag key`, `flag value type`, `evaluation context`, and the `default value`.
|
||||
|
||||
#### Requirement 4.1.2
|
||||
|
||||
> The `hook context` **SHOULD** provide: access to the `client metadata` and the `provider metadata` fields.
|
||||
|
||||
#### Requirement 4.1.3
|
||||
|
||||
> The `flag key`, `flag type`, and `default value` properties **MUST** be immutable. If the language does not support immutability, the hook **MUST NOT** modify these properties.
|
||||
|
||||
#### Requirement 4.1.4
|
||||
|
||||
> The evaluation context **MUST** be mutable only within the `before` hook.
|
||||
|
||||
### Hook Hints
|
||||
|
||||
#### Requirement 4.2.1
|
||||
|
||||
> `hook hints` **MUST** be a structure supports definition of arbitrary properties, with keys of type `string`, and values of type `boolean | string | number | datetime | structure`..
|
||||
|
||||
#### Condition 4.2.2
|
||||
|
||||
> The implementation language supports a mechanism for marking data as immutable.
|
||||
|
||||
##### Conditional Requirement 4.2.2.1
|
||||
|
||||
> Condition: `Hook hints` **MUST** be immutable.
|
||||
|
||||
##### Conditional Requirement 4.2.2.2
|
||||
|
||||
> Condition: The client `metadata` field in the `hook context` **MUST** be immutable.
|
||||
|
||||
##### Conditional Requirement 4.2.2.3
|
||||
|
||||
> Condition: The provider `metadata` field in the `hook context` **MUST** be immutable.
|
||||
|
||||
### Hook creation and parameters
|
||||
|
||||
#### Requirement 4.3.1
|
||||
|
||||
> Hooks **MUST** specify at least one stage.
|
||||
|
||||
#### Requirement 4.3.2
|
||||
|
||||
> The `before` stage **MUST** run before flag resolution occurs. It accepts a `hook context` (required) and `hook hints` (optional) as parameters and returns either an `evaluation context` or nothing.
|
||||
|
||||
```typescript
|
||||
EvaluationContext | void before(HookContext, HookHints);
|
||||
```
|
||||
|
||||
#### Requirement 4.3.3
|
||||
|
||||
> Any `evaluation context` returned from a `before` hook **MUST** be passed to subsequent `before` hooks (via `HookContext`).
|
||||
|
||||
#### Requirement 4.3.4
|
||||
|
||||
> When `before` hooks have finished executing, any resulting `evaluation context` **MUST** be merged with the invocation `evaluation context` with the invocation `evaluation context` taking precedence in the case of any conflicts.
|
||||
|
||||
#### Requirement 4.3.5
|
||||
|
||||
> The `after` stage **MUST** run after flag resolution occurs. It accepts a `hook context` (required), `flag evaluation details` (required) and `hook hints` (optional). It has no return value.
|
||||
|
||||
#### Requirement 4.3.6
|
||||
|
||||
> The `error` hook **MUST** run when errors are encountered in the `before` stage, the `after` stage or during flag resolution. It accepts `hook context` (required), `exception` representing what went wrong (required), and `hook hints` (optional). It has no return value.
|
||||
|
||||
#### Requirement 4.3.7
|
||||
|
||||
> The `finally` hook **MUST** run after the `before`, `after`, and `error` stages. It accepts a `hook context` (required) and `hook hints` (optional). There is no return value.
|
||||
|
||||
#### Condition 4.3.8
|
||||
|
||||
> `finally` is a reserved word in the language.
|
||||
|
||||
##### Conditional Requirement 4.3.8.1
|
||||
|
||||
> Instead of `finally`, `finallyAfter` **SHOULD** be used.
|
||||
|
||||
### Hook registration & ordering
|
||||
|
||||
#### Requirement 4.4.1
|
||||
|
||||
> The API, Client and invocation **MUST** have a method for registering hooks which accepts `flag evaluation options`
|
||||
|
||||
```js
|
||||
OpenFeature.addHooks(new Hook1());
|
||||
|
||||
//...
|
||||
|
||||
Client client = OpenFeature.getClient();
|
||||
client.addHooks(new Hook2());
|
||||
`
|
||||
//...
|
||||
|
||||
client.getValue('my-flag', 'defaultValue', new Hook3());
|
||||
```
|
||||
|
||||
#### Requirement 4.4.2
|
||||
|
||||
> Hooks **MUST** be evaluated in the following order:
|
||||
>
|
||||
> - before: API, Client, Invocation
|
||||
> - after: Invocation, Client, API
|
||||
> - error (if applicable): Invocation, Client, API
|
||||
> - finally: Invocation, Client, API
|
||||
|
||||
#### Requirement 4.4.3
|
||||
|
||||
> If a `finally` hook abnormally terminates, evaluation **MUST** proceed, including the execution of any remaining `finally` hooks.
|
||||
|
||||
In languages with try/catch semantics, this means that exceptions thrown in `finally` hooks should be caught, and not propagated up the call stack.
|
||||
|
||||
#### Requirement 4.4.4
|
||||
|
||||
> If an `error` hook abnormally terminates, evaluation **MUST** proceed, including the execution of any remaining `error` hooks.
|
||||
|
||||
In languages with try/catch semantics, this means that exceptions thrown in `error` hooks should be caught, and not propagated up the call stack.
|
||||
|
||||
#### Requirement 4.4.5
|
||||
|
||||
> If an error occurs in the `before` or `after` hooks, the `error` hooks **MUST** be invoked.
|
||||
|
||||
#### Requirement 4.4.6
|
||||
|
||||
> If an error occurs during the evaluation of `before` or `after` hooks, any remaining hooks in the `before` or `after` stages **MUST NOT** be invoked.
|
||||
|
||||
#### Requirement 4.4.7
|
||||
|
||||
> If an error occurs in the `before` hooks, the default value **MUST** be returned.
|
||||
|
||||
Before hooks can impact evaluation by various means, such as mutating the `evaluation context`. Therefore, an error in the `before` hooks is considered abnormal execution, and the default should be returned.
|
||||
|
||||
### [Flag evaluation options](./types.md#evaluation-options)
|
||||
|
||||
Usage might look something like:
|
||||
|
||||
```python
|
||||
val = client.get_boolean_value('my-key', False, evaluation_options={
|
||||
'hooks': new MyHook(),
|
||||
'hook_hints': {'side-item': 'onion rings'}
|
||||
})
|
||||
```
|
||||
|
||||
See: [Flag evaluation options](./flag-evaluation.md#)
|
||||
|
||||
#### Requirement 4.5.1
|
||||
|
||||
> `Flag evaluation options` **MAY** contain `hook hints`, a map of data to be provided to hook invocations.
|
||||
|
||||
#### Requirement 4.5.2
|
||||
|
||||
> `hook hints` **MUST** be passed to each hook.
|
||||
|
||||
#### Requirement 4.5.3
|
||||
|
||||
> The hook **MUST NOT** alter the `hook hints` structure.
|
|
@ -1,163 +0,0 @@
|
|||
# Provider
|
||||
|
||||
## Overview
|
||||
|
||||
The `provider` API defines interfaces that Provider Authors can use to abstract a particular flag management system, thus enabling the use of the `evaluation API` by Application Authors.
|
||||
|
||||
Providers are the "translator" between the flag evaluation calls made in application code, and the flag management system that stores flags and in some cases evaluates flags. At a minimum, providers should implement some basic evaluation methods which return flag values of the expected type. In addition, providers may transform the [evaluation context](evaluation-context.md) appropriately in order to be used in dynamic evaluation of their associated flag management system, provide insight into why evaluation proceeded the way it did, and expose configuration options for their associated flag management system. Hypothetical provider implementations might wrap a vendor SDK, embed an REST client, or read flags from a local file.
|
||||
|
||||

|
||||
|
||||
### Feature Provider Interface
|
||||
|
||||
#### Requirement 2.1
|
||||
|
||||
> The provider interface **MUST** define a `metadata` member or accessor, containing a `name` field or accessor of type string, which identifies the provider implementation.
|
||||
|
||||
```typescript
|
||||
provider.getMetadata().getName(); // "my-custom-provider"
|
||||
```
|
||||
|
||||
#### Flag Value Resolution
|
||||
|
||||
`Providers` are implementations of the `feature provider` interface, which may wrap vendor SDKs, REST API clients, or otherwise resolve flag values from the runtime environment.
|
||||
|
||||
##### Requirement 2.2
|
||||
|
||||
> The `feature provider` interface **MUST** define methods to resolve flag values, with parameters `flag key` (string, required), `default value` (boolean | number | string | structure, required), `evaluation context` (optional), and `evaluation options` (optional), which returns a `flag resolution` structure.
|
||||
|
||||
```typescript
|
||||
// example flag resolution function
|
||||
resolveBooleanValue(flagKey, defaultValue, context, options);
|
||||
```
|
||||
|
||||
see: [flag resolution structure](./types.md#flag-resolution), [flag value resolution](./glossary.md#flag-value-resolution)
|
||||
|
||||
##### Condition 2.3
|
||||
|
||||
> The implementing language type system differentiates between strings, numbers, booleans and structures.
|
||||
|
||||
###### Conditional Requirement 2.3.1
|
||||
|
||||
> The `feature provider` interface **MUST** define methods for typed flag resolution, including boolean, numeric, string, and structure.
|
||||
|
||||
```typescript
|
||||
// example boolean flag value resolution
|
||||
ResolutionDetails resolveBooleanValue(string flagKey, boolean defaultValue, context: EvaluationContext, options: FlagEvaluationOptions);
|
||||
|
||||
// example string flag value resolution
|
||||
ResolutionDetails resolveStringValue(string flagKey, string defaultValue, context: EvaluationContext, options: FlagEvaluationOptions);
|
||||
|
||||
// example number flag value resolution
|
||||
ResolutionDetails resolveNumberValue(string flagKey, number defaultValue, context: EvaluationContext, options: FlagEvaluationOptions);
|
||||
|
||||
// example structure flag value resolution
|
||||
ResolutionDetails resolveStructureValue(string flagKey, JsonObject defaultValue, context: EvaluationContext, options: FlagEvaluationOptions);
|
||||
```
|
||||
|
||||
##### Requirement 2.4
|
||||
|
||||
> In cases of normal execution, the `provider` **MUST** populate the `flag resolution` structure's `value` field with the resolved flag value.
|
||||
|
||||
##### Requirement 2.5
|
||||
|
||||
> In cases of normal execution, the `provider` **SHOULD** populate the `flag resolution` structure's `variant` field with a string identifier corresponding to the returned flag value.
|
||||
|
||||
For example, the flag value might be `3.14159265359`, and the variant field's value might be `"pi"`.
|
||||
|
||||
The value of the variant field might only be meaningful in the context of the flag management system associated with the provider. For example, the variant may be a UUID corresponding to the variant in the flag management system, or an index corresponding to the variant in the flag management system.
|
||||
|
||||
##### Requirement 2.6
|
||||
|
||||
> The `provider` **SHOULD** populate the `flag resolution` structure's `reason` field with a string indicating the semantic reason for the returned flag value.
|
||||
|
||||
Possible values vary by provider, but might include such values as `"TARGETING_MATCH"`, `"SPLIT"`, `"DISABLED"`, `"DEFAULT"`, `"UNKNOWN"` or `"ERROR"`.
|
||||
|
||||
##### Requirement 2.7
|
||||
|
||||
> In cases of normal execution, the `provider` **MUST NOT** populate the `flag resolution` structure's `error code` field, or otherwise must populate it with a null or falsy value.
|
||||
|
||||
##### Requirement 2.8
|
||||
|
||||
> In cases of abnormal execution, the `provider` **MUST** indicate an error using the idioms of the implementation language, with an associated error code having possible values `"PROVIDER_NOT_READY"`, `"FLAG_NOT_FOUND"`, `"PARSE_ERROR"`, `"TYPE_MISMATCH"`, or `"GENERAL"`.
|
||||
|
||||
The provider might throw an exception, return an error, or populate the `error code` object on the returned `flag resolution` structure to indicate a problem during flag value resolution.
|
||||
|
||||
##### Condition 2.9
|
||||
|
||||
> The implementation language supports generics (or an equivalent feature).
|
||||
|
||||
###### Conditional Requirement 2.9.1
|
||||
|
||||
> The `flag resolution` structure **SHOULD** accept a generic argument (or use an equivalent language feature) which indicates the type of the wrapped `value` field.
|
||||
|
||||
```typescript
|
||||
// example boolean flag value resolution with generic argument
|
||||
ResolutionDetails<boolean> resolveBooleanValue(string flagKey, boolean defaultValue, context: EvaluationContext, options: FlagEvaluationOptions);
|
||||
|
||||
// example string flag value resolution with generic argument
|
||||
ResolutionDetails<string> resolveStringValue(string flagKey, string defaultValue, context: EvaluationContext, options: FlagEvaluationOptions);
|
||||
|
||||
// example number flag value resolution with generic argument
|
||||
ResolutionDetails<number> resolveNumberValue(string flagKey, number defaultValue, context: EvaluationContext, options: FlagEvaluationOptions);
|
||||
|
||||
// example structure flag value resolution with generic argument
|
||||
ResolutionDetails<MyStruct> resolveStructureValue(string flagKey, MyStruct defaultValue, context: EvaluationContext, options: FlagEvaluationOptions);
|
||||
```
|
||||
|
||||
#### Context Transformation
|
||||
|
||||
Feature flag management systems often define structures representing arbitrary contextual data pertaining to the runtime, user, or application. The context transformer defines a simple interface to transform the OpenFeature `evaluation context` to such a structure, mapping values appropriately.
|
||||
|
||||
See [evaluation context](./evaluation-context.md).
|
||||
|
||||
##### Requirement 2.10
|
||||
|
||||
> The provider interface **MAY** define a `context transformer` method or function, which can be optionally implemented in order to transform the `evaluation context` prior to flag value resolution.
|
||||
|
||||
The OpenFeature `client` might apply the transformer function before passing the returned value (the `transformed context`) to the provider resolution methods, thus allowing the provider implementation to avoid implementing and calling such transformation logic repeatedly in flag value resolution methods.
|
||||
|
||||
```typescript
|
||||
class MyProvider implements Provider {
|
||||
//...
|
||||
|
||||
// implementation of context transformer
|
||||
MyProviderContext transformContext(EvaluationContext context) {
|
||||
return new MyProviderContext(context.email, context.ip, context.httpMethod);
|
||||
}
|
||||
|
||||
//...
|
||||
}
|
||||
```
|
||||
|
||||
See [evaluation context](./evaluation-context.md), [flag evaluation](./flag-evaluation.md#flag-evaluation).
|
||||
|
||||
##### Condition 2.11
|
||||
|
||||
> The implementation language supports generics (or an equivalent feature).
|
||||
|
||||
###### Conditional Requirement 2.11.1
|
||||
|
||||
> If the implementation includes a `context transformer`, the provider **SHOULD** accept a generic argument (or use an equivalent language feature) indicating the type of the transformed context.
|
||||
>
|
||||
> If such type information is supplied, more accurate type information can be supplied in the flag resolution methods.
|
||||
|
||||
```typescript
|
||||
// an example implementation in a language supporting interfaces, classes, and generics
|
||||
// T represents a generic argument for the type of the transformed context
|
||||
interface Provider<T> {
|
||||
|
||||
//...
|
||||
|
||||
// context transformer signature
|
||||
T transformContext(EvaluationContext context);
|
||||
|
||||
//...
|
||||
|
||||
// flag resolution methods context parameter type corresponds to class-generic
|
||||
boolean resolveBooleanValue (string flagKey, boolean defaultValue, T transformedContext, EvaluationOptions options);
|
||||
|
||||
//...
|
||||
|
||||
}
|
||||
```
|
|
@ -0,0 +1,532 @@
|
|||
---
|
||||
title: Flag Evaluation API
|
||||
description: The specification that defines the developer facing feature flag evaluation API.
|
||||
toc_max_heading_level: 4
|
||||
---
|
||||
|
||||
# 1. Flag Evaluation API
|
||||
|
||||
[](https://github.com/open-feature/spec/tree/main/specification#hardening)
|
||||
|
||||
## Overview
|
||||
|
||||
The `evaluation API` allows for the evaluation of feature flag values, independent of any flag control plane or vendor. In the absence of a [provider](./02-providers.md) the `evaluation API` uses the "No-op provider", which simply returns the supplied default flag value.
|
||||
|
||||
### 1.1. API Initialization and Configuration
|
||||
|
||||
#### Requirement 1.1.1
|
||||
|
||||
> The `API`, and any state it maintains **SHOULD** exist as a global singleton, even in cases wherein multiple versions of the `API` are present at runtime.
|
||||
|
||||
It's important that multiple instances of the `API` not be active, so that state stored therein, such as the registered `provider`, static global `evaluation context`, and globally configured `hooks` allow the `API` to behave predictably. This can be difficult in some runtimes or languages, but implementors should make their best effort to ensure that only a single instance of the `API` is used.
|
||||
|
||||
### Setting a provider
|
||||
|
||||
#### Requirement 1.1.2.1
|
||||
|
||||
> The `API` **MUST** define a `provider mutator`, a function to set the default `provider`, which accepts an API-conformant `provider` implementation.
|
||||
|
||||
```typescript
|
||||
// example provider mutator
|
||||
OpenFeature.setProvider(new MyProvider());
|
||||
```
|
||||
|
||||
The example above sets the default provider.
|
||||
This provider is used if a client is not bound to a specific provider via a [domain](../glossary.md#domain).
|
||||
|
||||
See [provider](./02-providers.md), [creating clients](#creating-clients) for details.
|
||||
|
||||
#### Requirement 1.1.2.2
|
||||
|
||||
> The `provider mutator` function **MUST** invoke the `initialize` function on the newly registered provider before using it to resolve flag values.
|
||||
|
||||
Application authors can await the newly set `provider's` readiness using the `PROVIDER_READY` event.
|
||||
Provider instances which are already active (because they have been bound to another `domain` or otherwise) need not be initialized again.
|
||||
The `provider's` readiness state can be determined from its `status` member/accessor.
|
||||
|
||||
See [event handlers and initialization](./05-events.md#event-handlers-and-initialization), [provider initialization](./02-providers.md#24-initialization), [domain](../glossary.md#domain) for details.
|
||||
|
||||
#### Requirement 1.1.2.3
|
||||
|
||||
> The `provider mutator` function **MUST** invoke the `shutdown` function on the previously registered provider once it's no longer being used to resolve flag values.
|
||||
|
||||
When a provider is no longer in use, it should be disposed of using its `shutdown` mechanism.
|
||||
Provider instances which are bound to multiple `domains` won't be shut down until the last binding is removed.
|
||||
|
||||
see [shutdown](./02-providers.md#25-shutdown), [setting a provider](#setting-a-provider), [domain](../glossary.md#domain) for details.
|
||||
|
||||
#### Requirement 1.1.2.4
|
||||
|
||||
> The `API` **SHOULD** provide functions to set a provider and wait for the `initialize` function to complete or abnormally terminate.
|
||||
|
||||
This function not only sets the provider, but ensures that the provider is ready (or in error) before returning or settling.
|
||||
|
||||
```java
|
||||
// default provider
|
||||
OpenFeatureAPI.getInstance().setProviderAndWait(myprovider); // this method blocks until the provider is ready or in error
|
||||
// client uses the default provider
|
||||
Client client = OpenFeatureAPI.getInstance().getClient();
|
||||
|
||||
// provider associated with domain-1
|
||||
OpenFeatureAPI.getInstance().setProviderAndWait('domain-1', myprovider); // this method blocks until the provider is ready or in error
|
||||
// client uses provider associated with the domain named 'domain-1'
|
||||
Client client = OpenFeatureAPI.getInstance().getClient('domain-1');
|
||||
```
|
||||
|
||||
Though it's possible to use [events](./05-events.md) to await provider readiness, such functions can make things simpler for `application authors` and `integrators`.
|
||||
Implementations indicate an error in a manner idiomatic to the language in use (returning an error, throwing an exception, etc).
|
||||
|
||||
#### Requirement 1.1.3
|
||||
|
||||
> The `API` **MUST** provide a function to bind a given `provider` to one or more clients using a `domain`. If the domain already has a bound provider, it is overwritten with the new mapping.
|
||||
|
||||
```java
|
||||
OpenFeature.setProvider("domain-1", new MyProvider());
|
||||
```
|
||||
|
||||
Clients can be associated with a particular provider by supplying a matching `domain` when the provider is set.
|
||||
|
||||
See [creating clients](#creating-clients), [domain](../glossary.md#domain) for details.
|
||||
|
||||
#### Requirement 1.1.4
|
||||
|
||||
> The `API` **MUST** provide a function to add `hooks` which accepts one or more API-conformant `hooks`, and appends them to the collection of any previously added hooks. When new hooks are added, previously added hooks are not removed.
|
||||
|
||||
```typescript
|
||||
// example hook attachment
|
||||
OpenFeature.addHooks([new MyHook()]);
|
||||
```
|
||||
|
||||
See [hooks](./04-hooks.md) for details.
|
||||
|
||||
#### Requirement 1.1.5
|
||||
|
||||
> The `API` **MUST** provide a function for retrieving the metadata field of the configured `provider`.
|
||||
|
||||
```typescript
|
||||
// example provider accessor
|
||||
OpenFeature.getProviderMetadata();
|
||||
```
|
||||
|
||||
It's possible to access provider metadata using a `domain`.
|
||||
If a provider has not been registered under the requested domain, the default provider metadata is returned.
|
||||
|
||||
```typescript
|
||||
// example provider accessor
|
||||
OpenFeature.getProviderMetadata("domain-1");
|
||||
```
|
||||
|
||||
See [provider](./02-providers.md), [domain](../glossary.md#domain) for details.
|
||||
|
||||
### Creating clients
|
||||
|
||||
#### Requirement 1.1.6
|
||||
|
||||
> The `API` **MUST** provide a function for creating a `client` which accepts the following options:
|
||||
>
|
||||
> - domain (optional): A logical string identifier for binding a client to a provider.
|
||||
|
||||
```java
|
||||
// example client creation and retrieval
|
||||
OpenFeature.getClient();
|
||||
```
|
||||
|
||||
It's possible to create a client that is associated with a `domain`.
|
||||
The client will use a provider in the same `domain` if one exists, otherwise, the default provider is used.
|
||||
|
||||
```java
|
||||
// example client creation and retrieval using a domain
|
||||
OpenFeature.getClient("domain-1");
|
||||
```
|
||||
|
||||
See [setting a provider](#setting-a-provider), [domain](../glossary.md#domain) for details.
|
||||
|
||||
#### Requirement 1.1.7
|
||||
|
||||
> The client creation function **MUST NOT** throw, or otherwise abnormally terminate.
|
||||
|
||||
Clients may be created in critical code paths, and even per-request in server-side HTTP contexts. Therefore, in keeping with the principle that OpenFeature should never cause abnormal execution of the first party application, this function should never throw. Abnormal execution in initialization should instead occur during provider registration.
|
||||
|
||||
### 1.2. Client Usage
|
||||
|
||||
#### Requirement 1.2.1
|
||||
|
||||
> The client **MUST** provide a method to add `hooks` which accepts one or more API-conformant `hooks`, and appends them to the collection of any previously added hooks. When new hooks are added, previously added hooks are not removed.
|
||||
|
||||
```typescript
|
||||
// example hook attachment
|
||||
client.addHooks([new MyHook()]);
|
||||
```
|
||||
|
||||
See [hooks](./04-hooks.md) for details.
|
||||
|
||||
#### Requirement 1.2.2
|
||||
|
||||
> The client interface **MUST** define a `metadata` member or accessor, containing an immutable `domain` field or accessor of type string, which corresponds to the `domain` value supplied during client creation.
|
||||
|
||||
```typescript
|
||||
client.getMetadata().getDomain(); // "domain-1"
|
||||
```
|
||||
|
||||
In previous drafts, this property was called `name`.
|
||||
For backwards compatibility, implementations should consider `name` an alias to `domain`.
|
||||
|
||||
### 1.3. Flag Evaluation
|
||||
|
||||
[](https://github.com/open-feature/spec/tree/main/specification#hardening)
|
||||
|
||||
#### Condition 1.3.1
|
||||
|
||||
> The implementation uses the dynamic-context paradigm.
|
||||
|
||||
see: [dynamic-context paradigm](../glossary.md#dynamic-context-paradigm)
|
||||
|
||||
##### Conditional Requirement 1.3.1.1
|
||||
|
||||
> The `client` **MUST** provide methods for typed flag evaluation, including boolean, numeric, string, and structure, with parameters `flag key` (string, required), `default value` (boolean | number | string | structure, required), `evaluation context` (optional), and `evaluation options` (optional), which returns the flag value.
|
||||
|
||||
```typescript
|
||||
// example boolean flag evaluation
|
||||
boolean myBool = client.getBooleanValue('bool-flag', false);
|
||||
|
||||
// example overloaded string flag evaluation with optional params
|
||||
string myString = client.getStringValue('string-flag', 'N/A', evaluationContext, options);
|
||||
|
||||
// example number flag evaluation
|
||||
number myNumber = client.getNumberValue('number-flag', 75);
|
||||
|
||||
// example overloaded structure flag evaluation with optional params
|
||||
MyStruct myStruct = client.getObjectValue<MyStruct>('structured-flag', { text: 'N/A', percentage: 75 }, evaluationContext, options);
|
||||
```
|
||||
|
||||
See [evaluation context](./03-evaluation-context.md) for details.
|
||||
|
||||
#### Condition 1.3.2
|
||||
|
||||
[](https://github.com/open-feature/spec/tree/main/specification#experimental)
|
||||
|
||||
> The implementation uses the static-context paradigm.
|
||||
|
||||
see: [static-context paradigm](../glossary.md#static-context-paradigm)
|
||||
|
||||
##### Conditional Requirement 1.3.2.1
|
||||
|
||||
> The `client` **MUST** provide methods for typed flag evaluation, including boolean, numeric, string, and structure, with parameters `flag key` (string, required), `default value` (boolean | number | string | structure, required), and `evaluation options` (optional), which returns the flag value.
|
||||
|
||||
```typescript
|
||||
// example boolean flag evaluation
|
||||
boolean myBool = client.getBooleanValue('bool-flag', false);
|
||||
|
||||
// example overloaded string flag evaluation with optional params
|
||||
string myString = client.getStringValue('string-flag', 'N/A', options);
|
||||
|
||||
// example number flag evaluation
|
||||
number myNumber = client.getNumberValue('number-flag', 75);
|
||||
|
||||
// example overloaded structure flag evaluation with optional params
|
||||
MyStruct myStruct = client.getObjectValue<MyStruct>('structured-flag', { text: 'N/A', percentage: 75 }, options);
|
||||
```
|
||||
|
||||
#### Condition 1.3.3
|
||||
|
||||
> The implementation language differentiates between floating-point numbers and integers.
|
||||
|
||||
##### Conditional Requirement 1.3.3.1
|
||||
|
||||
> The client **SHOULD** provide functions for floating-point numbers and integers, consistent with language idioms.
|
||||
|
||||
```java
|
||||
int getIntValue(String flag, int defaultValue);
|
||||
|
||||
long getFloatValue(String flag, long defaultValue);
|
||||
```
|
||||
|
||||
See [types](../types.md) for details.
|
||||
|
||||
#### Requirement 1.3.4
|
||||
|
||||
> The `client` **SHOULD** guarantee the returned value of any typed flag evaluation method is of the expected type. If the value returned by the underlying provider implementation does not match the expected type, it's to be considered abnormal execution, and the supplied `default value` should be returned.
|
||||
|
||||
### 1.4. Detailed Flag Evaluation
|
||||
|
||||
[](https://github.com/open-feature/spec/tree/main/specification#hardening)
|
||||
|
||||
The _detailed evaluation_ functions behave similarly to the standard flag evaluation functions, but provide additional metadata useful for telemetry, troubleshooting, debugging, and advanced integrations.
|
||||
|
||||
> [!NOTE]
|
||||
> Metadata included in the `evaluation details` should be considered "best effort", since not all providers will be able to provide these details (such as the reason a flag resolved to a particular value).
|
||||
|
||||
see: [evaluation details](../types.md#evaluation-details) type
|
||||
|
||||
#### Condition 1.4.1
|
||||
|
||||
> The implementation uses the dynamic-context paradigm.
|
||||
|
||||
see: [dynamic-context paradigm](../glossary.md#dynamic-context-paradigm)
|
||||
|
||||
##### Conditional Requirement 1.4.1.1
|
||||
|
||||
> The `client` **MUST** provide methods for detailed flag value evaluation with parameters `flag key` (string, required), `default value` (boolean | number | string | structure, required), `evaluation context` (optional), and `evaluation options` (optional), which returns an `evaluation details` structure.
|
||||
|
||||
```typescript
|
||||
// example detailed boolean flag evaluation
|
||||
FlagEvaluationDetails<boolean> myBoolDetails = client.getBooleanDetails('bool-flag', false);
|
||||
|
||||
// example detailed string flag evaluation
|
||||
FlagEvaluationDetails<string> myStringDetails = client.getStringDetails('string-flag', 'N/A', evaluationContext, options);
|
||||
|
||||
// example detailed number flag evaluation
|
||||
FlagEvaluationDetails<number> myNumberDetails = client.getNumberDetails('number-flag', 75);
|
||||
|
||||
// example detailed structure flag evaluation
|
||||
FlagEvaluationDetails<MyStruct> myStructDetails = client.getObjectDetails<MyStruct>('structured-flag', { text: 'N/A', percentage: 75 }, evaluationContext, options);
|
||||
|
||||
```
|
||||
|
||||
#### Condition 1.4.2
|
||||
|
||||
[](https://github.com/open-feature/spec/tree/main/specification#experimental)
|
||||
|
||||
> The implementation uses the static-context paradigm.
|
||||
|
||||
see: [static-context paradigm](../glossary.md#static-context-paradigm)
|
||||
|
||||
##### Conditional Requirement 1.4.2.1
|
||||
|
||||
> The `client` **MUST** provide methods for detailed flag value evaluation with parameters `flag key` (string, required), `default value` (boolean | number | string | structure, required), and `evaluation options` (optional), which returns an `evaluation details` structure.
|
||||
|
||||
```typescript
|
||||
// example detailed boolean flag evaluation
|
||||
FlagEvaluationDetails<boolean> myBoolDetails = client.getBooleanDetails('bool-flag', false);
|
||||
|
||||
// example detailed string flag evaluation
|
||||
FlagEvaluationDetails<string> myStringDetails = client.getStringDetails('string-flag', 'N/A', options);
|
||||
|
||||
// example detailed number flag evaluation
|
||||
FlagEvaluationDetails<number> myNumberDetails = client.getNumberDetails('number-flag', 75);
|
||||
|
||||
// example detailed structure flag evaluation
|
||||
FlagEvaluationDetails<MyStruct> myStructDetails = client.getObjectDetails<MyStruct>('structured-flag', { text: 'N/A', percentage: 75 }, options);
|
||||
|
||||
```
|
||||
|
||||
#### Requirement 1.4.3
|
||||
|
||||
> The `evaluation details` structure's `value` field **MUST** contain the evaluated flag value.
|
||||
|
||||
#### Condition 1.4.4
|
||||
|
||||
> The language supports generics (or an equivalent feature).
|
||||
|
||||
##### Conditional Requirement 1.4.4.1
|
||||
|
||||
> The `evaluation details` structure **SHOULD** accept a generic argument (or use an equivalent language feature) which indicates the type of the wrapped `value` field.
|
||||
|
||||
#### Requirement 1.4.5
|
||||
|
||||
> The `evaluation details` structure's `flag key` field **MUST** contain the `flag key` argument passed to the detailed flag evaluation method.
|
||||
|
||||
#### Requirement 1.4.6
|
||||
|
||||
> In cases of normal execution, the `evaluation details` structure's `variant` field **MUST** contain the value of the `variant` field in the `flag resolution` structure returned by the configured `provider`, if the field is set.
|
||||
|
||||
#### Requirement 1.4.7
|
||||
|
||||
> In cases of normal execution, the `evaluation details` structure's `reason` field **MUST** contain the value of the `reason` field in the `flag resolution` structure returned by the configured `provider`, if the field is set.
|
||||
|
||||
#### Requirement 1.4.8
|
||||
|
||||
> In cases of abnormal execution, the `evaluation details` structure's `error code` field **MUST** contain an `error code`.
|
||||
|
||||
See [error code](../types.md#error-code) for details.
|
||||
|
||||
#### Requirement 1.4.9
|
||||
|
||||
> In cases of abnormal execution (network failure, unhandled error, etc) the `reason` field in the `evaluation details` **SHOULD** indicate an error.
|
||||
|
||||
#### Requirement 1.4.10
|
||||
|
||||
> Methods, functions, or operations on the client **MUST NOT** throw exceptions, or otherwise abnormally terminate. Flag evaluation calls must always return the `default value` in the event of abnormal execution. Exceptions include functions or methods for the purposes for configuration or setup.
|
||||
|
||||
Configuration code includes code to set the provider, instantiate providers, and configure the global API object.
|
||||
|
||||
#### Requirement 1.4.11
|
||||
|
||||
> Methods, functions, or operations on the client **SHOULD NOT** write log messages.
|
||||
|
||||
The client methods (particularly the evaluation methods) run in hot code paths.
|
||||
Logging (even at error level) can cause a huge volume of log entries.
|
||||
For example, in a circumstance in which an application expecting a particular flag to exist is deployed in advance of that flag's being defined in the management system, logs can become inundated with `FLAG_NOT_FOUND` messages and related stack traces.
|
||||
Logging in these code paths is highly discouraged.
|
||||
Application authors can attach a [logging hook](../appendix-a-included-utilities.md#logging-hook) or author their own custom logging hook(s) to help with debugging or satisfy their particular logging needs.
|
||||
|
||||
Logging is encouraged in functions to do with configuration, initialization, shutdown, etc.
|
||||
|
||||
#### Requirement 1.4.12
|
||||
|
||||
> The `client` **SHOULD** provide asynchronous or non-blocking mechanisms for flag evaluation.
|
||||
|
||||
It's recommended to provide non-blocking mechanisms for flag evaluation, particularly in languages or environments wherein there's a single thread of execution.
|
||||
|
||||
#### Requirement 1.4.13
|
||||
|
||||
> In cases of abnormal execution, the `evaluation details` structure's `error message` field **MAY** contain a string containing additional details about the nature of the error.
|
||||
|
||||
#### Requirement 1.4.14
|
||||
|
||||
> If the `flag metadata` field in the `flag resolution` structure returned by the configured `provider` is set, the `evaluation details` structure's `flag metadata` field **MUST** contain that value. Otherwise, it **MUST** contain an empty record.
|
||||
|
||||
This `flag metadata` field is intended as a mechanism for providers to surface additional information about a feature flag (or its evaluation) beyond what is defined within the OpenFeature spec itself. The primary consumer of this information is a provider-specific hook.
|
||||
|
||||
#### Condition 1.4.15
|
||||
|
||||
> The implementation language supports a mechanism for marking data as immutable.
|
||||
|
||||
##### Conditional Requirement 1.4.14.1
|
||||
|
||||
> Condition: `Flag metadata` **MUST** be immutable.
|
||||
|
||||
### Evaluation Options
|
||||
|
||||
#### Requirement 1.5.1
|
||||
|
||||
> The `evaluation options` structure's `hooks` field denotes an ordered collection of hooks that the client **MUST** execute for the respective flag evaluation, in addition to those already configured.
|
||||
|
||||
See [hooks](./04-hooks.md) for details.
|
||||
|
||||
### 1.6. Shutdown
|
||||
|
||||
[](https://github.com/open-feature/spec/tree/main/specification#experimental)
|
||||
|
||||
The API's `shutdown` function defines a means of graceful shutdown, calling the `shutdown` function on all providers, allowing them to flush telemetry, clean up connections, and release any relevant resources.
|
||||
It also provides a means of resetting the API object to its default state, removing all hooks, event handlers, providers, and setting a "No-op provider"; this is useful for testing purposes.
|
||||
It's recommended that application-authors call this function on application shutdown, and after the completion of test suites which make use of the SDK.
|
||||
|
||||
#### Requirement 1.6.1
|
||||
|
||||
> The API **MUST** define a function to propagate a shutdown request to all providers.
|
||||
|
||||
The global API object defines a `shutdown` function, which will call the respective `shutdown` function on all providers.
|
||||
Alternatively, implementations might leverage language idioms such as auto-disposable interfaces or some means of cancellation signal propagation to allow for graceful shutdown.
|
||||
This shutdown function unconditionally calls the shutdown function on all registered providers, regardless of their state.
|
||||
|
||||
see: [`shutdown`](./02-providers.md#25-shutdown)
|
||||
|
||||
#### Requirement 1.6.2
|
||||
|
||||
> The API's `shutdown` function **MUST** reset all state of the API, removing all hooks, event handlers, and providers.
|
||||
|
||||
After shutting down all providers, the `shutdown` function resets the state of the API.
|
||||
This is especially useful for testing purposes to restore the API to a known state.
|
||||
|
||||
### 1.7. Provider Lifecycle Management
|
||||
|
||||
The implementation maintains an internal representation of the state of configured providers, tracking the lifecycle of each provider.
|
||||
This state of the provider is exposed on associated `clients`.
|
||||
|
||||
The diagram below illustrates the possible states and transitions of the `state` field for a provider during the provider lifecycle.
|
||||
|
||||
```mermaid
|
||||
---
|
||||
title: Provider lifecycle
|
||||
---
|
||||
stateDiagram-v2
|
||||
direction LR
|
||||
[*] --> NOT_READY
|
||||
NOT_READY --> READY:initialize()
|
||||
NOT_READY --> ERROR:initialize()
|
||||
NOT_READY --> FATAL:initialize()
|
||||
FATAL --> [*]
|
||||
READY --> ERROR:#ast;
|
||||
ERROR --> READY:#ast;
|
||||
READY --> STALE:#ast;
|
||||
STALE --> READY:#ast;
|
||||
STALE --> ERROR:#ast;
|
||||
READY --> NOT_READY:shutdown()
|
||||
STALE --> NOT_READY:shutdown()
|
||||
ERROR --> NOT_READY:shutdown()
|
||||
READY --> RECONCILING:::client:setContext()
|
||||
RECONCILING:::client --> READY
|
||||
RECONCILING:::client --> ERROR
|
||||
|
||||
classDef client fill:#888
|
||||
```
|
||||
|
||||
\* transitions occurring when associated events are spontaneously emitted from the provider
|
||||
|
||||
<span style={{ color: '#888' }}>█</span> only defined in static-context (client-side) paradigm
|
||||
|
||||
> [!NOTE]
|
||||
> Only SDKs implementing the [static context (client-side) paradigm](../glossary.md#static-context-paradigm) define `RECONCILING` to facilitate [context reconciliation](./02-providers.md#26-provider-context-reconciliation).
|
||||
|
||||
#### Requirement 1.7.1
|
||||
|
||||
> The `client` **MUST** define a `provider status` accessor which indicates the readiness of the associated provider, with possible values `NOT_READY`, `READY`, `STALE`, `ERROR`, or `FATAL`.
|
||||
|
||||
The SDK at all times maintains an up-to-date state corresponding to the success/failure of the last lifecycle method (`initialize`, `shutdown`, `on context change`) or emitted event.
|
||||
|
||||
see [provider status](../types.md#provider-status)
|
||||
|
||||
#### Condition 1.7.2
|
||||
|
||||
[](https://github.com/open-feature/spec/tree/main/specification#experimental)
|
||||
|
||||
> The implementation uses the static-context paradigm.
|
||||
|
||||
see: [static-context paradigm](../glossary.md#static-context-paradigm)
|
||||
|
||||
##### Conditional Requirement 1.7.2.1
|
||||
|
||||
> In addition to `NOT_READY`, `READY`, `STALE`, or `ERROR`, the `provider status` accessor must support possible value `RECONCILING`.
|
||||
|
||||
In the static context paradigm, the implementation must define a `provider status` indicating that a provider is reconciling its internal state due to a context change.
|
||||
|
||||
#### Requirement 1.7.3
|
||||
|
||||
> The client's `provider status` accessor **MUST** indicate `READY` if the `initialize` function of the associated provider terminates normally.
|
||||
|
||||
Once the provider has initialized, the `provider status` should indicate the provider is ready to be used to evaluate flags.
|
||||
|
||||
#### Requirement 1.7.4
|
||||
|
||||
> The client's `provider status` accessor **MUST** indicate `ERROR` if the `initialize` function of the associated provider terminates abnormally.
|
||||
|
||||
If the provider has failed to initialize, the `provider status` should indicate the provider is in an error state.
|
||||
|
||||
#### Requirement 1.7.5
|
||||
|
||||
> The client's `provider status` accessor **MUST** indicate `FATAL` if the `initialize` function of the associated provider terminates abnormally and indicates `error code` `PROVIDER_FATAL`.
|
||||
|
||||
If the provider has failed to initialize, the `provider status` should indicate the provider is in an error state.
|
||||
|
||||
#### Requirement 1.7.6
|
||||
|
||||
> The client **MUST** default, run error hooks, and indicate an error if flag resolution is attempted while the provider is in `NOT_READY`.
|
||||
|
||||
The client defaults and returns the `PROVIDER_NOT_READY` `error code` if evaluation is attempted before the provider is initialized (the provider is still in a `NOT_READY` state).
|
||||
The SDK avoids calling the provider's resolver functions entirely ("short-circuits") if the provider is in this state.
|
||||
|
||||
see: [error codes](../types.md#error-code), [flag value resolution](./02-providers.md#22-flag-value-resolution)
|
||||
|
||||
#### Requirement 1.7.7
|
||||
|
||||
> The client **MUST** default, run error hooks, and indicate an error if flag resolution is attempted while the provider is in `FATAL`.
|
||||
|
||||
The client defaults and returns the `PROVIDER_FATAL` `error code` if evaluation is attempted after the provider has transitioned to an irrecoverable error state.
|
||||
The SDK avoids calling the provider's resolver functions entirely ("short-circuits") if the provider is in this state.
|
||||
|
||||
see: [error codes](../types.md#error-code), [flag value resolution](./02-providers.md#22-flag-value-resolution)
|
||||
|
||||
#### Requirement 1.7.8
|
||||
|
||||
> Implementations **SHOULD** propagate the `error code` returned from any provider lifecycle methods.
|
||||
|
||||
The SDK ensures that if the provider's lifecycle methods terminate with an `error code`, that error code is included in any associated error events and returned/thrown errors/exceptions.
|
||||
|
||||
see: [error codes](../types.md#error-code)
|
||||
|
||||
#### Requirement 1.7.9
|
||||
|
||||
> The client's `provider status` accessor **MUST** indicate `NOT_READY` once the `shutdown` function of the associated provider terminates.
|
||||
|
||||
Regardless of the success of the provider's `shutdown` function, the `provider status` should convey the provider is no longer ready to use once the shutdown function terminates.
|
|
@ -0,0 +1,307 @@
|
|||
---
|
||||
title: Provider
|
||||
description: The specification that defines the interfaces, behaviors and responsibilities of providers.
|
||||
toc_max_heading_level: 4
|
||||
---
|
||||
|
||||
# 2. Provider
|
||||
|
||||
[](https://github.com/open-feature/spec/tree/main/specification#hardening)
|
||||
|
||||
## Overview
|
||||
|
||||
The `provider` API defines interfaces that Provider Authors can use to abstract a particular flag management system, thus enabling the use of the `evaluation API` by Application Authors.
|
||||
|
||||
Providers are the "translator" between the flag evaluation calls made in application code, and the flag management system that stores flags and in some cases evaluates flags. At a minimum, providers should implement some basic evaluation methods which return flag values of the expected type. In addition, providers may transform the [evaluation context](./03-evaluation-context.md) appropriately in order to be used in dynamic evaluation of their associated flag management system, provide insight into why evaluation proceeded the way it did, and expose configuration options for their associated flag management system. Hypothetical provider implementations might wrap a vendor SDK, embed an REST client, or read flags from a local file.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[Application] --> API(Evaluation API)
|
||||
API --> P[Provider]
|
||||
P --> FMS[(Flag Management System)]
|
||||
```
|
||||
|
||||
### 2.1. Feature Provider Interface
|
||||
|
||||
#### Requirement 2.1.1
|
||||
|
||||
> The provider interface **MUST** define a `metadata` member or accessor, containing a `name` field or accessor of type string, which identifies the provider implementation.
|
||||
|
||||
```typescript
|
||||
provider.getMetadata().getName(); // "my-custom-provider"
|
||||
```
|
||||
|
||||
### 2.2 Flag Value Resolution
|
||||
|
||||
`Providers` are implementations of the `feature provider` interface, which may wrap vendor SDKs, REST API clients, or otherwise resolve flag values from the runtime environment.
|
||||
|
||||
#### Requirement 2.2.1
|
||||
|
||||
> The `feature provider` interface **MUST** define methods to resolve flag values, with parameters `flag key` (string, required), `default value` (boolean | number | string | structure, required) and `evaluation context` (optional), which returns a `resolution details` structure.
|
||||
|
||||
```typescript
|
||||
// example flag resolution function
|
||||
resolveBooleanValue(flagKey, defaultValue, context);
|
||||
```
|
||||
|
||||
see: [flag resolution structure](../types.md#resolution-details), [flag value resolution](../glossary.md#resolving-flag-values)
|
||||
|
||||
#### Condition 2.2.2
|
||||
|
||||
> The implementing language type system differentiates between strings, numbers, booleans and structures.
|
||||
|
||||
##### Conditional Requirement 2.2.2.1
|
||||
|
||||
> The `feature provider` interface **MUST** define methods for typed flag resolution, including boolean, numeric, string, and structure.
|
||||
|
||||
```typescript
|
||||
// example boolean flag value resolution
|
||||
ResolutionDetails resolveBooleanValue(string flagKey, boolean defaultValue, context: EvaluationContext);
|
||||
|
||||
// example string flag value resolution
|
||||
ResolutionDetails resolveStringValue(string flagKey, string defaultValue, context: EvaluationContext);
|
||||
|
||||
// example number flag value resolution
|
||||
ResolutionDetails resolveNumberValue(string flagKey, number defaultValue, context: EvaluationContext);
|
||||
|
||||
// example structure flag value resolution
|
||||
ResolutionDetails resolveStructureValue(string flagKey, JsonObject defaultValue, context: EvaluationContext);
|
||||
```
|
||||
|
||||
#### Requirement 2.2.3
|
||||
|
||||
> In cases of normal execution, the `provider` **MUST** populate the `resolution details` structure's `value` field with the resolved flag value.
|
||||
|
||||
#### Requirement 2.2.4
|
||||
|
||||
> In cases of normal execution, the `provider` **SHOULD** populate the `resolution details` structure's `variant` field with a string identifier corresponding to the returned flag value.
|
||||
|
||||
For example, the flag value might be `3.14159265359`, and the variant field's value might be `"pi"`.
|
||||
|
||||
The value of the variant field might only be meaningful in the context of the flag management system associated with the provider. For example, the variant may be a UUID corresponding to the variant in the flag management system, or an index corresponding to the variant in the flag management system.
|
||||
|
||||
#### Requirement 2.2.5
|
||||
|
||||
> The `provider` **SHOULD** populate the `resolution details` structure's `reason` field with `"STATIC"`, `"DEFAULT",` `"TARGETING_MATCH"`, `"SPLIT"`, `"CACHED"`, `"DISABLED"`, `"UNKNOWN"`, `"STALE"`, `"ERROR"` or some other string indicating the semantic reason for the returned flag value.
|
||||
|
||||
As indicated in the definition of the [`resolution details`](../types.md#resolution-details) structure, the `reason` should be a string. This allows providers to reflect accurately why a flag was resolved to a particular value.
|
||||
|
||||
#### Requirement 2.2.6
|
||||
|
||||
> In cases of normal execution, the `provider` **MUST NOT** populate the `resolution details` structure's `error code` field, or otherwise must populate it with a null or falsy value.
|
||||
|
||||
#### Requirement 2.2.7
|
||||
|
||||
> In cases of abnormal execution, the `provider` **MUST** indicate an error using the idioms of the implementation language, with an associated `error code` and optional associated `error message`.
|
||||
|
||||
The provider might throw an exception, return an error, or populate the `error code` object on the returned `resolution details` structure to indicate a problem during flag value resolution.
|
||||
|
||||
See [error code](../types.md#error-code) for details.
|
||||
|
||||
```typescript
|
||||
// example throwing an exception with an error code and optional error message.
|
||||
throw new ProviderError(ErrorCode.INVALID_CONTEXT, "The 'foo' attribute must be a string.");
|
||||
```
|
||||
|
||||
#### Condition 2.2.8
|
||||
|
||||
> The implementation language supports generics (or an equivalent feature).
|
||||
|
||||
##### Conditional Requirement 2.2.8.1
|
||||
|
||||
> The `resolution details` structure **SHOULD** accept a generic argument (or use an equivalent language feature) which indicates the type of the wrapped `value` field.
|
||||
|
||||
```typescript
|
||||
// example boolean flag value resolution with generic argument
|
||||
ResolutionDetails<boolean> resolveBooleanValue(string flagKey, boolean defaultValue, context: EvaluationContext);
|
||||
|
||||
// example string flag value resolution with generic argument
|
||||
ResolutionDetails<string> resolveStringValue(string flagKey, string defaultValue, context: EvaluationContext);
|
||||
|
||||
// example number flag value resolution with generic argument
|
||||
ResolutionDetails<number> resolveNumberValue(string flagKey, number defaultValue, context: EvaluationContext);
|
||||
|
||||
// example structure flag value resolution with generic argument
|
||||
ResolutionDetails<MyStruct> resolveStructureValue(string flagKey, MyStruct defaultValue, context: EvaluationContext);
|
||||
```
|
||||
|
||||
#### Requirement 2.2.9
|
||||
|
||||
> The `provider` **SHOULD** populate the `resolution details` structure's `flag metadata` field.
|
||||
|
||||
#### Requirement 2.2.10
|
||||
|
||||
> `flag metadata` **MUST** be a structure supporting the definition of arbitrary properties, with keys of type `string`, and values of type `boolean | string | number`.
|
||||
|
||||
### 2.3. Provider hooks
|
||||
|
||||
A `provider hook` exposes a mechanism for `provider authors` to register [`hooks`](./04-hooks.md) to tap into various stages of the flag evaluation lifecycle. These hooks can be used to perform side effects and mutate the context for purposes of the provider. Provider hooks are not configured or controlled by the `application author`.
|
||||
|
||||
#### Requirement 2.3.1
|
||||
|
||||
> The provider interface **MUST** define a `provider hook` mechanism which can be optionally implemented in order to add `hook` instances to the evaluation life-cycle.
|
||||
|
||||
```java
|
||||
class MyProvider implements Provider {
|
||||
//...
|
||||
|
||||
readonly hooks: Hook[] = [new MyProviderHook()];
|
||||
|
||||
// ..or alternatively..
|
||||
getProviderHooks(): Hook[] {
|
||||
return [new MyProviderHook()];
|
||||
}
|
||||
|
||||
//...
|
||||
}
|
||||
```
|
||||
|
||||
#### Requirement 2.3.2
|
||||
|
||||
> In cases of normal execution, the `provider` **MUST NOT** populate the `resolution details` structure's `error message` field, or otherwise must populate it with a null or falsy value.
|
||||
|
||||
#### Requirement 2.3.3
|
||||
|
||||
> In cases of abnormal execution, the `resolution details` structure's `error message` field **MAY** contain a string containing additional detail about the nature of the error.
|
||||
|
||||
### 2.4 Initialization
|
||||
|
||||
[](https://github.com/open-feature/spec/tree/main/specification#experimental)
|
||||
|
||||
#### Requirement 2.4.1
|
||||
|
||||
> The `provider` **MAY** define an initialization function which accepts the global `evaluation context` as an argument and performs initialization logic relevant to the provider.
|
||||
|
||||
Many feature flag frameworks or SDKs require some initialization before they can be used.
|
||||
They might require the completion of an HTTP request, establishing persistent connections, or starting timers or worker threads.
|
||||
The initialization function is an ideal place for such logic.
|
||||
|
||||
```java
|
||||
// MyProvider implementation of the initialize function defined in Provider
|
||||
class MyProvider implements Provider {
|
||||
//...
|
||||
|
||||
// the global context is passed to the initialization function
|
||||
void initialize(EvaluationContext initialContext) {
|
||||
/*
|
||||
A hypothetical initialization function: make an initial call doing some bulk initial evaluation, start a worker to do periodic updates
|
||||
*/
|
||||
this.flagCache = this.restClient.bulkEvaluate(initialContext);
|
||||
this.startPolling();
|
||||
}
|
||||
|
||||
//...
|
||||
}
|
||||
```
|
||||
|
||||
#### Condition 2.4.2
|
||||
|
||||
> The provider defines an `initialize` function.
|
||||
|
||||
##### Conditional Requirement 2.4.2.1
|
||||
|
||||
> If the provider's `initialize` function fails to render the provider ready to evaluate flags, it **SHOULD** abnormally terminate.
|
||||
|
||||
If a provider is unable to start up correctly, it should indicate abnormal execution by throwing an exception, returning an error, or otherwise indicating so by means idiomatic to the implementation language.
|
||||
If the error is irrecoverable (perhaps due to bad credentials or invalid configuration) the `PROVIDER_FATAL` error code should be used.
|
||||
|
||||
see: [error codes](../types.md#error-code)
|
||||
|
||||
### 2.5. Shutdown
|
||||
|
||||
[](https://github.com/open-feature/spec/tree/main/specification#experimental)
|
||||
|
||||
#### Requirement 2.5.1
|
||||
|
||||
> The provider **MAY** define a mechanism to gracefully shutdown and dispose of resources.
|
||||
|
||||
```java
|
||||
// MyProvider implementation of the dispose function defined in Provider
|
||||
class MyProvider implements Provider, AutoDisposable {
|
||||
//...
|
||||
void dispose() {
|
||||
// close connections, terminate threads or timers, etc...
|
||||
}
|
||||
```
|
||||
|
||||
#### Requirement 2.5.2
|
||||
|
||||
> After a provider's `shutdown` function has terminated, the provider **SHOULD** revert to its uninitialized state.
|
||||
|
||||
If a provider requires initialization, once it's shut down, it must transition to its uninitialized state.
|
||||
Some providers may allow reinitialization from this state.
|
||||
Providers not requiring initialization are assumed to be ready at all times.
|
||||
Providers in the process of initializing abort initialization if shutdown is called while they are still starting up.
|
||||
|
||||
see: [initialization](#24-initialization)
|
||||
|
||||
#### Requirement 2.5.3
|
||||
|
||||
> A Provider's `shutdown` function **SHOULD** be idempotent.
|
||||
|
||||
If a provider's `shutdown` function has been called, subsequent calls (without an intervening call to `initialize`) should have no effect.
|
||||
|
||||
see: [initialization](#24-initialization)
|
||||
|
||||
### 2.6. Provider context reconciliation
|
||||
|
||||
[](https://github.com/open-feature/spec/tree/main/specification#experimental)
|
||||
|
||||
Static-context focused providers may need a mechanism to understand when their cache of evaluated flags must be invalidated or updated. An `on context changed` function can be defined which performs whatever operations are needed to reconcile the evaluated flags with the new context.
|
||||
|
||||
#### Requirement 2.6.1
|
||||
|
||||
> The provider **MAY** define an `on context changed` function, which takes an argument for the previous context and the newly set context, in order to respond to an evaluation context change.
|
||||
|
||||
Especially in static-context implementations, providers and underlying SDKs may maintain state for a particular context.
|
||||
The `on context changed` function provides a mechanism to update this state, often by re-evaluating flags in bulk with respect to the new context.
|
||||
|
||||
```java
|
||||
// MyProvider implementation of the onContextChanged function defined in Provider
|
||||
class MyProvider implements Provider {
|
||||
//...
|
||||
|
||||
onContextChanged(EvaluationContext oldContext, EvaluationContext newContext): void {
|
||||
// update context-sensitive cached flags, or otherwise react to the change in the global context
|
||||
}
|
||||
|
||||
//...
|
||||
}
|
||||
```
|
||||
|
||||
Providers may maintain remote connections, timers, threads or other constructs that need to be appropriately disposed of.
|
||||
Provider authors may implement a `shutdown` function to perform relevant clean-up actions.
|
||||
Alternatively, implementations might leverage language idioms such as auto-disposable interfaces or some means of cancellation signal propagation to allow for graceful shutdown.
|
||||
|
||||
### 2.7. Tracking Support
|
||||
|
||||
[](https://github.com/open-feature/spec/tree/main/specification#experimental)
|
||||
|
||||
Some flag management systems support tracking functionality, which can be used to associate feature flag evaluations with subsequent user actions or application state.
|
||||
|
||||
See [tracking](./06-tracking.md).
|
||||
|
||||
#### Condition 2.7.1
|
||||
|
||||
> The `provider` **MAY** define a function for tracking the occurrence of a particular user action or application state, with parameters `tracking event name` (string, required), `evaluation context` (optional) and `tracking event details` (optional) which returns nothing.
|
||||
|
||||
```java
|
||||
class MyProvider implements Tracking {
|
||||
//...
|
||||
|
||||
/**
|
||||
* Record a tracking event.
|
||||
*/
|
||||
public void track(String trackingEventName, EvaluationContext context, TrackingEventDetails details) {
|
||||
// perform side effects to record the event
|
||||
}
|
||||
|
||||
//...
|
||||
}
|
||||
```
|
||||
|
||||
The track function is a void function (function returning nothing).
|
||||
The track function performs side effects required to record the `tracking event` in question, which may include network activity or other I/O; this I/O should not block the function call.
|
||||
Providers should be careful to complete any communication or flush any relevant uncommitted tracking data before they shut down.
|
||||
|
||||
See [shutdown](#25-shutdown).
|
|
@ -0,0 +1,219 @@
|
|||
---
|
||||
title: Evaluation Context
|
||||
description: The specification that defines the structure and expectations of evaluation context.
|
||||
toc_max_heading_level: 4
|
||||
---
|
||||
|
||||
# 3. Evaluation Context
|
||||
|
||||
[](https://github.com/open-feature/spec/tree/main/specification#experimental)
|
||||
|
||||
## Overview
|
||||
|
||||
The `evaluation context` provides ambient information for the purposes of flag evaluation. Contextual data may be used as the basis for targeting, including rule-based evaluation, overrides for specific subjects, or fractional flag evaluation.
|
||||
|
||||
The context might contain information about the end-user, the application, the host, or any other ambient data that might be useful in flag evaluation. For example, a flag system might define rules that return a specific value based on the user's email address, locale, or the time of day. The context provides this information. The context can be optionally provided at evaluation, and mutated in [before hooks](./04-hooks.md).
|
||||
|
||||
### 3.1 Fields
|
||||
|
||||
> [!NOTE]
|
||||
> Field casing is not specified and should be chosen in accordance with language idioms.
|
||||
|
||||
see: [types](../types.md)
|
||||
|
||||
#### Requirement 3.1.1
|
||||
|
||||
> The `evaluation context` structure **MUST** define an optional `targeting key` field of type string, identifying the subject of the flag evaluation.
|
||||
|
||||
The targeting key uniquely identifies the subject (end-user, or client service) of a flag evaluation. Providers may require this field for fractional flag evaluation, rules, or overrides targeting specific users. Such providers may behave unpredictably if a targeting key is not specified at flag resolution.
|
||||
|
||||
#### Requirement 3.1.2
|
||||
|
||||
> The evaluation context **MUST** support the inclusion of custom fields, having keys of type `string`, and values of type `boolean | string | number | datetime | structure`.
|
||||
|
||||
see: [structure](../types.md#structure), [datetime](../types.md#datetime)
|
||||
|
||||
#### Requirement 3.1.3
|
||||
|
||||
> The evaluation context **MUST** support fetching the custom fields by key and also fetching all key value pairs.
|
||||
|
||||
#### Requirement 3.1.4
|
||||
|
||||
> The evaluation context fields **MUST** have a unique key.
|
||||
|
||||
The key uniquely identifies a field in the `evaluation context` and it should be unique across all types to avoid any collision when marshalling the `evaluation context` by the provider.
|
||||
|
||||
### 3.2 Context levels and merging
|
||||
|
||||
#### Condition 3.2.1
|
||||
|
||||
> The implementation uses the dynamic-context paradigm.
|
||||
|
||||
see: [dynamic-context paradigm](../glossary.md#dynamic-context-paradigm)
|
||||
|
||||
##### Conditional Requirement 3.2.1.1
|
||||
|
||||
> The API, Client and invocation **MUST** have a method for supplying `evaluation context`.
|
||||
|
||||
API (global) `evaluation context` can be used to supply static data to flag evaluation, such as an application identifier, compute region, or hostname. Client and invocation `evaluation context` are ideal for dynamic data, such as end-user attributes.
|
||||
|
||||
#### Condition 3.2.2
|
||||
|
||||
[](https://github.com/open-feature/spec/tree/main/specification#experimental)
|
||||
|
||||
> The implementation uses the static-context paradigm.
|
||||
|
||||
see: [static-context paradigm](../glossary.md#static-context-paradigm)
|
||||
|
||||
##### Conditional Requirement 3.2.2.1
|
||||
|
||||
> The API **MUST** have a method for setting the global `evaluation context`.
|
||||
|
||||
API (global) `evaluation context` can be used to supply data to flag evaluation, such as (but not limited to) user name, email, or user organization membership changes.
|
||||
|
||||
##### Conditional Requirement 3.2.2.2
|
||||
|
||||
> The Client and invocation **MUST NOT** have a method for supplying `evaluation context`.
|
||||
|
||||
In the static-context paradigm, context is global. The client and invocation cannot supply evaluation context.
|
||||
|
||||
##### Conditional Requirement 3.2.2.3
|
||||
|
||||
> The API **MUST** have a method for setting `evaluation context` for a `domain`.
|
||||
|
||||
In the static-context paradigm, provider specific context can be set using the associated `domain`.
|
||||
The global context is used if there is no matching provider specific context.
|
||||
|
||||
See [setting a provider](./01-flag-evaluation.md#setting-a-provider), [domain](../glossary.md#domain) for details.
|
||||
|
||||
##### Conditional Requirement 3.2.2.4
|
||||
|
||||
> The API **MUST** have a mechanism to manage `evaluation context` for an associated `domain`.
|
||||
|
||||
In the static-context paradigm, it's possible to create and remove provider-specific context.
|
||||
See [setting a provider](./01-flag-evaluation.md#setting-a-provider), [domain](../glossary.md#domain) for details.
|
||||
|
||||
#### Requirement 3.2.3
|
||||
|
||||
> 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 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)")
|
||||
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
|
||||
|
||||
[](https://github.com/open-feature/spec/tree/main/specification#experimental)
|
||||
|
||||
> The implementation uses the static-context paradigm.
|
||||
|
||||
see: [static-context paradigm](../glossary.md#static-context-paradigm)
|
||||
|
||||
##### Conditional Requirement 3.2.4.1
|
||||
|
||||
> When the global `evaluation context` is set, the `on context changed` function **MUST** run.
|
||||
|
||||
The SDK implementation must run the `on context changed` function on all registered providers that use the global `evaluation context` whenever it is mutated.
|
||||
|
||||
##### Conditional Requirement 3.2.4.2
|
||||
|
||||
> When the `evaluation context` for a specific provider is set, the `on context changed` function **MUST** only run on the associated provider.
|
||||
|
||||
The SDK implementation must run the `on context changed` function only on the provider that is scoped to the mutated `evaluation context`.
|
||||
|
||||
### 3.3 Context Propagation
|
||||
|
||||
[](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<EvaluationContext>();
|
||||
|
||||
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.
|
|
@ -0,0 +1,409 @@
|
|||
---
|
||||
title: Hooks
|
||||
description: The specification that defines the expectations and life cycle of hooks.
|
||||
toc_max_heading_level: 4
|
||||
---
|
||||
|
||||
# 4. Hooks
|
||||
|
||||
[](https://github.com/open-feature/spec/tree/main/specification#experimental)
|
||||
|
||||
## Overview
|
||||
|
||||
`Hooks` are a mechanism whereby application developers can add arbitrary behavior to flag evaluation. They operate similarly to middleware in many web frameworks.
|
||||
|
||||
Hooks add their logic at any of four specific stages of flag evaluation:
|
||||
|
||||
- `before`, immediately before flag evaluation
|
||||
- `after`, immediately after successful flag evaluation
|
||||
- `error`, immediately after an unsuccessful flag evaluation
|
||||
- `finally`, unconditionally after flag evaluation
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
B(('Before' stage)) ---> FE[Flag Evaluation]
|
||||
B -..->|Error| E
|
||||
FE ---> A(('After' stage))
|
||||
FE -..->|Error| E(('Error' stage))
|
||||
A ---> F(('Finally' stage))
|
||||
E -..-> F
|
||||
```
|
||||
|
||||
Hooks can be configured to run globally (impacting all flag evaluations), per client, or per flag evaluation invocation. Some example use cases for a hook include adding additional data to the [evaluation context](./03-evaluation-context.md), performing validation on the received flag value, providing data to telemetric tools, and logging errors.
|
||||
|
||||
### 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.
|
||||
|
||||
### 4.1. Hook context
|
||||
|
||||
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`, `default value`, and `hook data`.
|
||||
|
||||
The `evaluation context` provided in the hook context refers to the **merged evaluation context** as specified in [Requirement 3.2.3](./03-evaluation-context.md#requirement-323).
|
||||
|
||||
#### Requirement 4.1.2
|
||||
|
||||
> The `hook context` **SHOULD** provide access to the `client metadata` and the `provider metadata` fields.
|
||||
|
||||
#### Requirement 4.1.3
|
||||
|
||||
> The `flag key`, `flag type`, and `default value` properties **MUST** be immutable. If the language does not support immutability, the hook **MUST NOT** modify these properties.
|
||||
|
||||
#### Condition 4.1.4
|
||||
|
||||
> The implementation uses the dynamic-context paradigm.
|
||||
|
||||
see: [dynamic-context paradigm](../glossary.md#dynamic-context-paradigm)
|
||||
|
||||
##### Conditional Requirement 4.1.4.1
|
||||
|
||||
> 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:
|
||||
|
||||
```js
|
||||
hookContext.hookData = {'my-key': 'my-value'}
|
||||
```
|
||||
|
||||
Mutable content:
|
||||
|
||||
```js
|
||||
hookContext.hookData.set('my-key', 'my-value')
|
||||
```
|
||||
|
||||
### 4.2. Hook Hints
|
||||
|
||||
#### Requirement 4.2.1
|
||||
|
||||
> `hook hints` **MUST** be a structure supports definition of arbitrary properties, with keys of type `string`, and values of type `boolean | string | number | datetime | structure`.
|
||||
|
||||
#### Condition 4.2.2
|
||||
|
||||
> The implementation language supports a mechanism for marking data as immutable.
|
||||
|
||||
##### Conditional Requirement 4.2.2.1
|
||||
|
||||
> Condition: `Hook hints` **MUST** be immutable.
|
||||
|
||||
##### Conditional Requirement 4.2.2.2
|
||||
|
||||
> Condition: The client `metadata` field in the `hook context` **MUST** be immutable.
|
||||
|
||||
##### Conditional Requirement 4.2.2.3
|
||||
|
||||
> Condition: The provider `metadata` field in the `hook context` **MUST** be immutable.
|
||||
|
||||
### 4.3. Hook creation and parameters
|
||||
|
||||
#### Requirement 4.3.1
|
||||
|
||||
> 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.
|
||||
|
||||
see: [dynamic-context paradigm](../glossary.md#dynamic-context-paradigm)
|
||||
|
||||
##### Conditional Requirement 4.3.2.1
|
||||
|
||||
> The `before` stage **MUST** run before flag resolution occurs. It accepts a `hook context` (required) and `hook hints` (optional) as parameters and returns either an `evaluation context` or nothing.
|
||||
|
||||
```java
|
||||
EvaluationContext | void before(HookContext hookContext, HookHints hints);
|
||||
```
|
||||
|
||||
#### Condition 4.3.3
|
||||
|
||||
[](https://github.com/open-feature/spec/tree/main/specification#experimental)
|
||||
|
||||
> The implementation uses the static-context paradigm.
|
||||
|
||||
see: [static-context paradigm](../glossary.md#static-context-paradigm)
|
||||
|
||||
##### Conditional Requirement 4.3.3.1
|
||||
|
||||
> The `before` stage **MUST** run before flag resolution occurs. It accepts a `hook context` (required) and `hook hints` (optional) as parameters. It has no return value.
|
||||
|
||||
```java
|
||||
void before(HookContext hookContext, HookHints hints);
|
||||
```
|
||||
|
||||
#### Requirement 4.3.4
|
||||
|
||||
> Any `evaluation context` returned from a `before` hook **MUST** be passed to subsequent `before` hooks (via `HookContext`).
|
||||
|
||||
#### Requirement 4.3.5
|
||||
|
||||
> When `before` hooks have finished executing, any resulting `evaluation context` **MUST** be merged with the existing `evaluation context`.
|
||||
|
||||
Evaluation context merge order is defined in [Context levels and merging](./03-evaluation-context.md#32-context-levels-and-merging).
|
||||
|
||||
#### Requirement 4.3.6
|
||||
|
||||
> The `after` stage **MUST** run after flag resolution occurs. It accepts a `hook context` (required), `evaluation details` (required) and `hook hints` (optional). It has no return value.
|
||||
|
||||
#### Requirement 4.3.7
|
||||
|
||||
> The `error` hook **MUST** run when errors are encountered in the `before` stage, the `after` stage or during flag resolution. It accepts `hook context` (required), `exception` representing what went wrong (required), and `hook hints` (optional). It has no return value.
|
||||
|
||||
#### Requirement 4.3.8
|
||||
|
||||
> The `finally` hook **MUST** run after the `before`, `after`, and `error` stages. It accepts a `hook context` (required), `evaluation details` (required) and `hook hints` (optional). It has no return value.
|
||||
|
||||
The evaluation details passed to the `finally` stage matches the evaluation details returned to the application author.
|
||||
|
||||
#### Condition 4.3.9
|
||||
|
||||
> `finally` is a reserved word in the language.
|
||||
|
||||
##### Conditional Requirement 4.3.9.1
|
||||
|
||||
> Instead of `finally`, `finallyAfter` **SHOULD** be used.
|
||||
|
||||
### 4.4. Hook registration & ordering
|
||||
|
||||
#### Requirement 4.4.1
|
||||
|
||||
> The API, Client, Provider, and invocation **MUST** have a method for registering hooks.
|
||||
|
||||
```js
|
||||
OpenFeature.addHooks(new Hook1());
|
||||
|
||||
//...
|
||||
|
||||
Client client = OpenFeature.getClient();
|
||||
client.addHooks(new Hook2());
|
||||
`
|
||||
//...
|
||||
|
||||
client.getValue('my-flag', 'defaultValue', new Hook3());
|
||||
```
|
||||
|
||||
#### Requirement 4.4.2
|
||||
|
||||
> Hooks **MUST** be executed "stack-wise" with respect to flag resolution, prioritizing increasing specificity (API, Client, Invocation, Provider) first, and the order in which they were added second.
|
||||
|
||||
Before flag resolution (the `before` stage), hooks run in the order `API` -> `Client` -> `Invocation` -> `Provider`, and within those, in the order in which they were added.
|
||||
After flag evaluation (the `after`, `error`, or `finally` stages), hooks run in the order `Provider` -> `Invocation` -> `Client` -> `API`, and within those, in reverse of the order in which they were added.
|
||||
This achieves intuitive "stack-like" or "LIFO" behavior for side effects and transformations.
|
||||
|
||||
Given hooks A - H, each implementing the both the `before` and `after` stages, added at the following levels and order:
|
||||
|
||||
- API: [A, B]
|
||||
- Client: [C, D]
|
||||
- Invocation: [E, F]
|
||||
- Provider: [G, H]
|
||||
|
||||
The expected order of execution is:
|
||||
|
||||
```mermaid
|
||||
flowchart BT
|
||||
subgraph FlagResolution [Flag Resolution]
|
||||
flagResolution[flagResolution]
|
||||
end
|
||||
|
||||
subgraph Provider [Provider Layer]
|
||||
G_before[G.before]
|
||||
H_before[H.before]
|
||||
G_after[G.after]
|
||||
H_after[H.after]
|
||||
end
|
||||
|
||||
subgraph Invocation [Invocation Layer]
|
||||
E_before[E.before]
|
||||
F_before[F.before]
|
||||
E_after[E.after]
|
||||
F_after[F.after]
|
||||
end
|
||||
|
||||
subgraph Client [Client Layer]
|
||||
C_before[C.before]
|
||||
D_before[D.before]
|
||||
C_after[C.after]
|
||||
D_after[D.after]
|
||||
end
|
||||
|
||||
subgraph API [API Layer]
|
||||
A_before[A.before]
|
||||
B_before[B.before]
|
||||
A_after[A.after]
|
||||
B_after[B.after]
|
||||
end
|
||||
|
||||
A_before --> B_before
|
||||
B_before --> C_before
|
||||
C_before --> D_before
|
||||
D_before --> E_before
|
||||
E_before --> F_before
|
||||
F_before --> G_before
|
||||
G_before --> H_before
|
||||
H_before --> flagResolution
|
||||
|
||||
flagResolution --> H_after
|
||||
H_after --> G_after
|
||||
G_after --> F_after
|
||||
F_after --> E_after
|
||||
E_after --> D_after
|
||||
D_after --> C_after
|
||||
C_after --> B_after
|
||||
B_after --> A_after
|
||||
```
|
||||
|
||||
#### Requirement 4.4.3
|
||||
|
||||
> If a `finally` hook abnormally terminates, evaluation **MUST** proceed, including the execution of any remaining `finally` hooks.
|
||||
|
||||
In languages with try/catch semantics, this means that exceptions thrown in `finally` hooks should be caught and not propagated up the call stack.
|
||||
|
||||
#### Requirement 4.4.4
|
||||
|
||||
> If an `error` hook abnormally terminates, evaluation **MUST** proceed, including the execution of any remaining `error` hooks.
|
||||
|
||||
In languages with try/catch semantics, this means that exceptions thrown in `error` hooks should be caught, and not propagated up the call stack.
|
||||
|
||||
#### Requirement 4.4.5
|
||||
|
||||
> If an error occurs in the `before` or `after` hooks, the `error` hooks **MUST** be invoked.
|
||||
|
||||
#### Requirement 4.4.6
|
||||
|
||||
> If an error occurs during the evaluation of `before` or `after` hooks, any remaining hooks in the `before` or `after` stages **MUST NOT** be invoked.
|
||||
|
||||
#### Requirement 4.4.7
|
||||
|
||||
> If an error occurs in the `before` hooks, the default value **MUST** be returned.
|
||||
|
||||
Before hooks can impact evaluation by various means, such as mutating the `evaluation context`. Therefore, an error in the `before` hooks is considered abnormal execution, and the default should be returned.
|
||||
|
||||
### [Flag evaluation options](../types.md#evaluation-options)
|
||||
|
||||
Usage might look something like:
|
||||
|
||||
```python
|
||||
val = client.get_boolean_value('my-key', False, evaluation_options={
|
||||
'hooks': new MyHook(),
|
||||
'hook_hints': {'side-item': 'onion rings'}
|
||||
})
|
||||
```
|
||||
|
||||
see: [Flag evaluation options](./01-flag-evaluation.md#evaluation-options)
|
||||
|
||||
#### Requirement 4.5.1
|
||||
|
||||
> `Flag evaluation options` **MAY** contain `hook hints`, a map of data to be provided to hook invocations.
|
||||
|
||||
#### Requirement 4.5.2
|
||||
|
||||
> `hook hints` **MUST** be passed to each hook.
|
||||
|
||||
#### 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<EvaluationContext> 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 supporting the definition of arbitrary properties, with keys of type `string`, and values of any type.
|
||||
|
||||
Access to hook data is restricted to only a single hook instance, and it has no serialization requirements, and as a result does not require any value type restrictions.
|
||||
|
||||
Example TypeScript definition:
|
||||
|
||||
```js
|
||||
type HookData = Record<string, unknown>;
|
||||
```
|
|
@ -0,0 +1,249 @@
|
|||
---
|
||||
title: Events
|
||||
description: Specification defining event semantics
|
||||
toc_max_heading_level: 4
|
||||
---
|
||||
|
||||
# 5. Events
|
||||
|
||||
[](https://github.com/open-feature/spec/tree/main/specification#experimental)
|
||||
|
||||
## Overview
|
||||
|
||||
`Events` allow consumers (_application integrator_, _application author_, _integration author_) to react to state changes in the provider or underlying flag management system, such as flag definition changes, provider readiness, or error conditions. A provider may emit events or run a callback indicating that it received a certain event, optionally providing data associated with that event. Handlers registered on the `client` or the global `API` are then invoked with this data.
|
||||
|
||||
The data that providers supply in event payloads may include a list of `flag keys` changed, error messages, and possibly updated flag values.
|
||||
|
||||
```mermaid
|
||||
graph
|
||||
P(Provider) -->|emit event| A[API]
|
||||
A -->|run handlers| AH("API (global) event handlers")
|
||||
A --> C[Client]
|
||||
C -->|run handlers| CH(Client event handlers)
|
||||
```
|
||||
|
||||
The `domain` of a provider constitutes a logical scope for events.
|
||||
Clients associated with a particular provider through a `domain` run event handlers only when that provider emits events, or one of its lifecycle functions terminates.
|
||||
|
||||
see: [domain](../glossary.md#domain)
|
||||
|
||||
### 5.1. Provider events
|
||||
|
||||
#### Requirement 5.1.1
|
||||
|
||||
> The `provider` **MAY** define a mechanism for signaling the occurrence of one of a set of events, including `PROVIDER_READY`, `PROVIDER_ERROR`, `PROVIDER_CONFIGURATION_CHANGED` and `PROVIDER_STALE`, with a `provider event details` payload.
|
||||
|
||||
Providers cannot emit `PROVIDER_CONTEXT_CHANGED` or `PROVIDER_RECONCILING` events.
|
||||
These are emitted only by the SDK during context reconciliation.
|
||||
|
||||
If available, native event-emitter or observable/observer language constructs can be used.
|
||||
|
||||
When a provider is unable to evaluate flags (perhaps due to loss of connection with a remote service) the provider can signal this by emitting a `PROVIDER_ERROR` event.
|
||||
When it recovers, it can emit a `PROVIDER_READY` event.
|
||||
If the error state is irrecoverable, the `PROVIDER_FATAL` error code can be used.
|
||||
If a provider caches rules-sets or previously evaluated flags, and such states cannot be considered up-to-date, the provider can signal this by emitting a `PROVIDER_STALE` event.
|
||||
|
||||
see: [provider event types](../types.md#provider-events), [`event details`](../types.md#provider-event-details), [events handlers and context reconciliation](#event-handlers-and-context-reconciliation)
|
||||
|
||||
#### Requirement 5.1.2
|
||||
|
||||
> When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers **MUST** run.
|
||||
|
||||
Client event handlers respect the dynamic binding of clients to providers via `domains`.
|
||||
Client event handlers run when a lifecycle function terminates on the associated provider, or the associated provider emits an event.
|
||||
|
||||
see: [provider event types](./../types.md#provider-events) and [event handlers](#52-event-handlers).
|
||||
|
||||
#### Requirement 5.1.3
|
||||
|
||||
> When a `provider` signals the occurrence of a particular `event`, event handlers on clients which are not associated with that provider **MUST NOT** run.
|
||||
|
||||
Client event handlers respect the dynamic binding of clients to providers via `domains`.
|
||||
Client event handlers do not run when a lifecycle function terminates on an unassociated provider, or an unassociated provider emits an event.
|
||||
|
||||
see [setting a provider](./01-flag-evaluation.md#setting-a-provider), [domain](../glossary.md#domain) for details.
|
||||
|
||||
#### Requirement 5.1.4
|
||||
|
||||
> `PROVIDER_ERROR` events **SHOULD** populate the `provider event details`'s `error message` field.
|
||||
|
||||
The error message field should contain an informative message as to the nature of the error.
|
||||
|
||||
See [event metadata](../types.md#provider-event-details)
|
||||
|
||||
#### Requirement 5.1.5
|
||||
|
||||
> `PROVIDER_ERROR` events **SHOULD** populate the `provider event details`'s `error code` field.
|
||||
|
||||
See [event metadata](../types.md#provider-event-details)
|
||||
|
||||
### 5.2. Event handlers
|
||||
|
||||
#### Requirement 5.2.1
|
||||
|
||||
> The `client` **MUST** provide a function for associating `handler functions` with a particular `provider event type`.
|
||||
|
||||
```java
|
||||
// run the myClientOnReadyHandler function when the PROVIDER_READY event is fired
|
||||
client.addHandler(ProviderEvents.Ready, myClientOnReadyHandler);
|
||||
```
|
||||
|
||||
see: [provider events](#51-provider-events), [`provider event types`](../types.md#provider-events)
|
||||
|
||||
#### Requirement 5.2.2
|
||||
|
||||
> The `API` **MUST** provide a function for associating `handler functions` with a particular `provider event type`.
|
||||
|
||||
```java
|
||||
// run the myGlobalErrorHandler function when the PROVIDER_READY event is fired
|
||||
OpenFeature.addHandler(ProviderEvents.Error, myGlobalErrorHandler);
|
||||
```
|
||||
|
||||
see: [provider events](#51-provider-events), [`provider event types`](../types.md#provider-events)
|
||||
|
||||
#### Requirement 5.2.3
|
||||
|
||||
> The `event details` **MUST** contain the `provider name` associated with the event.
|
||||
|
||||
The `provider name` indicates the provider from which the event originated.
|
||||
This is especially relevant for global event handlers used for general monitoring, such as alerting on provider errors.
|
||||
|
||||
See [setting a provider](./01-flag-evaluation.md#setting-a-provider), [creating clients](./01-flag-evaluation.md#creating-clients).
|
||||
|
||||
#### Requirement 5.2.4
|
||||
|
||||
> The `handler function` **MUST** accept an `event details` parameter.
|
||||
|
||||
see: [`event details`](../types.md#event-details)
|
||||
|
||||
#### Requirement 5.2.5
|
||||
|
||||
> If a `handler function` terminates abnormally, other `handler functions` **MUST** run.
|
||||
|
||||
#### Requirement 5.2.6
|
||||
|
||||
> Event handlers **MUST** persist across `provider` changes.
|
||||
|
||||
If the underlying provider is changed, existing client and API event handlers will still fire.
|
||||
This means that the order of provider configuration and event handler addition is independent.
|
||||
|
||||
#### Requirement 5.2.7
|
||||
|
||||
> The `API` and `client` **MUST** provide a function allowing the removal of event handlers.
|
||||
|
||||
```java
|
||||
// remove an existing handler for a PROVIDER_CONFIGURATION_CHANGED event
|
||||
client.removeHandler(ProviderEvents.ConfigurationChanged, myClientOnChangedHandler);
|
||||
```
|
||||
|
||||
### Event handlers and initialization
|
||||
|
||||
Though providers themselves need not implement events, the `flag evaluation API` uses events to convey relevant state changes during configuration and initialization.
|
||||
Implementations automatically emit `PROVIDER_READY` or `PROVIDER_ERROR` events depending on the outcome of the `initialize` function, if the provider has implemented one (if none is implemented, `PROVIDER_READY` runs unconditionally).
|
||||
_Application authors_ and _application integrators_ use these events to wait for proper initialization of the provider and to do basic monitoring and error handling.
|
||||
|
||||
#### Requirement 5.3.1
|
||||
|
||||
> If the provider's `initialize` function terminates normally, `PROVIDER_READY` handlers **MUST** run.
|
||||
|
||||
See [provider initialization](./02-providers.md#24-initialization) and [setting a provider](./01-flag-evaluation.md#setting-a-provider).
|
||||
|
||||
#### Requirement 5.3.2
|
||||
|
||||
> If the provider's `initialize` function terminates abnormally, `PROVIDER_ERROR` handlers **MUST** run.
|
||||
|
||||
A failed initialization could represent an unrecoverable error, such as bad credentials or a missing file.
|
||||
If a failed initialization could also represent a transient error.
|
||||
A provider which maintains a persistent connection to a remote `flag management system` may attempt to reconnect, and emit `PROVIDER_READY` after a failed initialization.
|
||||
|
||||
See [provider initialization](./02-providers.md#24-initialization) and [setting a provider](./01-flag-evaluation.md#setting-a-provider).
|
||||
|
||||
#### Requirement 5.3.3
|
||||
|
||||
> Handlers attached after the provider is already in the associated state, **MUST** run immediately.
|
||||
|
||||
Handlers may be attached at any point in the application lifecycle.
|
||||
Handlers should run immediately if the provider is already in the associated state.
|
||||
For instance, _application authors_ may attach readiness handlers to be confident that the system is ready to evaluate flags.
|
||||
If such handlers are attached after the provider underlying the client has already been initialized, they should run immediately.
|
||||
|
||||
See [provider initialization](./02-providers.md#24-initialization), [setting a provider](./01-flag-evaluation.md#setting-a-provider).
|
||||
|
||||
### Event handlers and context reconciliation
|
||||
|
||||
Providers built to conform to the static context paradigm feature two additional events: `PROVIDER_RECONCILING` and `PROVIDER_CONTEXT_CHANGED`.
|
||||
When the provider is reconciling its internal state (the `on context changed` function is running and not yet terminated), the SDK emits `PROVIDER_RECONCILING` and transitions the provider into state `RECONCILING`.
|
||||
This can be particularly useful for displaying loading indicators while the [evaluation context](./03-evaluation-context.md) is being reconciled.
|
||||
|
||||
If the `on context changed` function terminates normally, the SDK emits (`PROVIDER_CONTEXT_CHANGED`) and transitions the provider into the `READY` state, otherwise it emits `PROVIDER_ERROR` and transitions the provider into `ERROR` state.
|
||||
The `PROVIDER_CONTEXT_CHANGED` is used to signal that the associated context has been changed, and flags should be re-evaluated.
|
||||
This can be particularly useful for triggering UI repaints in multiple components when one component updates the [evaluation context](./03-evaluation-context.md).
|
||||
|
||||
```mermaid
|
||||
---
|
||||
title: Provider context reconciliation
|
||||
---
|
||||
stateDiagram-v2
|
||||
direction TB
|
||||
READY --> READY:emit(PROVIDER_CONTEXT_CHANGED)*
|
||||
READY --> RECONCILING:emit(PROVIDER_RECONCILING)
|
||||
RECONCILING --> READY:emit(PROVIDER_CONTEXT_CHANGED)
|
||||
RECONCILING --> ERROR:emit(PROVIDER_ERROR)
|
||||
```
|
||||
|
||||
\* Implementations may allow for providers to reconcile synchronously, in which case no `PROVIDER_RECONCILING` event is emitted.
|
||||
|
||||
#### Condition 5.3.4
|
||||
|
||||
[](https://github.com/open-feature/spec/tree/main/specification#experimental)
|
||||
|
||||
> The implementation uses the static-context paradigm.
|
||||
|
||||
see: [static-context paradigm](../glossary.md#static-context-paradigm)
|
||||
|
||||
##### Conditional Requirement 5.3.4.1
|
||||
|
||||
> While the provider's `on context changed` function is executing, associated `RECONCILING` handlers **MUST** run.
|
||||
|
||||
The implementation must run any `RECONCILING` handlers associated with the provider while the provider is reconciling its state.
|
||||
In languages with asynchronous semantics, the emission of this event can be skipped if the `on context changed` function of the provider in question executes synchronously for a given provider, no other operations can take place while it runs.
|
||||
|
||||
see: [provider event types](../types.md#provider-events), [provider events](#51-provider-events), [provider context reconciliation](02-providers.md#26-provider-context-reconciliation)
|
||||
|
||||
##### Conditional Requirement 5.3.4.2
|
||||
|
||||
> If the provider's `on context changed` function terminates normally, and no other invocations have yet to terminate, associated `PROVIDER_CONTEXT_CHANGED` handlers **MUST** run.
|
||||
|
||||
The implementation must run any `PROVIDER_CONTEXT_CHANGED` handlers associated with the provider after the provider has reconciled its state and returned from the `on context changed` function.
|
||||
The `PROVIDER_CONTEXT_CHANGED` is not emitted from the provider itself; the SDK implementation must run the `PROVIDER_CONTEXT_CHANGED` handlers if the `on context changed` function terminates normally.
|
||||
It's possible that the `on context changed` function is invoked simultaneously or in quick succession; in this case the SDK will only run the `PROVIDER_CONTEXT_CHANGED` handlers after all reentrant invocations have terminated, and the last to terminate was successful (terminated normally).
|
||||
see: [provider event types](../types.md#provider-events), [provider events](#51-provider-events), [provider context reconciliation](02-providers.md#26-provider-context-reconciliation)
|
||||
|
||||
##### Conditional Requirement 5.3.4.3
|
||||
|
||||
> If the provider's `on context changed` function terminates abnormally, and no other invocations have yet to terminate, associated `PROVIDER_ERROR` handlers **MUST** run.
|
||||
|
||||
The `PROVIDER_ERROR` is not emitted from the provider itself; the SDK implementation must run the `PROVIDER_ERROR` handlers if the `on context changed` throws or otherwise signals an error.
|
||||
It's possible that the `on context changed` function is invoked simultaneously or in quick succession; in this case the SDK will only run the `PROVIDER_ERROR` handlers after all reentrant invocations have terminated, and the last to terminate was unsuccessful (terminated abnormally).
|
||||
|
||||
see: [provider event types](../types.md#provider-events), [provider events](#51-provider-events), [provider context reconciliation](02-providers.md#26-provider-context-reconciliation)
|
||||
|
||||
#### Requirement 5.3.5
|
||||
|
||||
> If the provider emits an event, the value of the client's `provider status` **MUST** be updated accordingly.
|
||||
|
||||
Some providers may emit events spontaneously, based on changes in their internal state (connections, caches, etc).
|
||||
The SDK must update its internal representation of the provider's state accordingly:
|
||||
|
||||
| Event | Associated Status |
|
||||
| -------------------------------- | ------------------------------------------------------- |
|
||||
| `PROVIDER_READY` | `READY` |
|
||||
| `PROVIDER_STALE` | `STALE` |
|
||||
| `PROVIDER_ERROR` | `ERROR`/`FATAL`* |
|
||||
| `PROVIDER_CONFIGURATION_CHANGED` | N/A (provider remains in its current state) |
|
||||
| `PROVIDER_CONTEXT_CHANGED` | N/A (only emitted by SDK during context reconciliation) |
|
||||
| `PROVIDER_RECONCILING` | N/A (only emitted by SDK during context reconciliation) |
|
||||
|
||||
\* If the `error code` associated with the error indicates `PROVIDER_FATAL`, the state is set to `FATAL`
|
||||
|
||||
see: [provider lifecycle management](01-flag-evaluation.md#17-provider-lifecycle-management), [provider status](../types.md#provider-status) [error codes](../types.md#error-code)
|
|
@ -0,0 +1,107 @@
|
|||
---
|
||||
title: Tracking
|
||||
description: Specification defining tracking API
|
||||
toc_max_heading_level: 4
|
||||
---
|
||||
|
||||
# 6. Tracking
|
||||
|
||||
[](https://github.com/open-feature/spec/tree/main/specification#experimental)
|
||||
|
||||
## Overview
|
||||
|
||||
The `tracking API` enables the association of feature flag evaluations with subsequent actions or application states, in order to facilitate experimentation and analysis of the impact of feature flags on business objectives.
|
||||
|
||||
Combined with hooks which report feature flag evaluations to the analytics platform in question, tracking can allow for robust experimentation even for flag management systems that don't support tracking directly.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
Evaluation API->>+Tracking Hook: evaluate
|
||||
Tracking Hook->>Analytics Platform: before
|
||||
Tracking Hook->>Analytics Platform: after
|
||||
Tracking Hook->>-Evaluation API: evaluate
|
||||
Evaluation API->>Analytics Platform: track
|
||||
```
|
||||
|
||||
### 6.1. Tracking API
|
||||
|
||||
#### Condition 6.1.1
|
||||
|
||||
> The implementation uses the dynamic-context paradigm.
|
||||
|
||||
see: [dynamic-context paradigm](../glossary.md#dynamic-context-paradigm)
|
||||
|
||||
##### Conditional Requirement 6.1.1.1
|
||||
|
||||
> The `client` **MUST** define a function for tracking the occurrence of a particular action or application state, with parameters `tracking event name` (string, required), `evaluation context` (optional) and `tracking event details` (optional), which returns nothing.
|
||||
|
||||
```java
|
||||
// example tracking event recording that a subject reached a page associated with a business goal
|
||||
client.track("visited-promo-page", evaluationContext);
|
||||
|
||||
// example tracking event recording that a subject performed an action associated with a business goal, with the tracking event details having a particular numeric value
|
||||
client.track("clicked-checkout", evaluationContext, new TrackingEventDetails(99.77));
|
||||
|
||||
// example tracking event recording that a subject performed an action associated with a business goal, with the tracking event details having a particular numeric value
|
||||
client.track("clicked-checkout", evaluationContext, new TrackingEventDetails(99.77).add("currencyCode", "USD"));
|
||||
```
|
||||
|
||||
See [evaluation context](../types.md#evaluation-context), [tracking event details](#62-tracking-event-details).
|
||||
|
||||
#### Condition 6.1.2
|
||||
|
||||
[](https://github.com/open-feature/spec/tree/main/specification#experimental)
|
||||
|
||||
> The implementation uses the static-context paradigm.
|
||||
|
||||
see: [static-context paradigm](../glossary.md#static-context-paradigm)
|
||||
|
||||
##### Conditional Requirement 6.1.2.1
|
||||
|
||||
> The `client` **MUST** define a function for tracking the occurrence of a particular action or application state, with parameters `tracking event name` (string, required) and `tracking event details` (optional), which returns nothing.
|
||||
|
||||
The track function is a void function (function returning nothing).
|
||||
Though it may be associated with network activity or other I/O, it need not be awaited by application authors.
|
||||
|
||||
```java
|
||||
// example tracking event recording that a subject reached a page associated with a business goal
|
||||
client.track("visited-promo-page");
|
||||
|
||||
// example tracking event recording that a subject performed an action associated with a business goal, with the tracking event details having a particular numeric value
|
||||
client.track("clicked-checkout", new TrackingEventDetails(99.77));
|
||||
|
||||
// example tracking event recording that a subject performed an action associated with a business goal, with the tracking event details having a particular numeric and some additional details
|
||||
client.track("clicked-checkout", new TrackingEventDetails(99.77).add("currencyCode", "USD"));
|
||||
```
|
||||
|
||||
#### Requirement 6.1.3
|
||||
|
||||
> The evaluation context passed to the provider's track function **MUST** be merged in the order: API (global; lowest precedence) -> transaction -> client -> invocation (highest precedence), with duplicate values being overwritten.
|
||||
|
||||
The SDK passes a merged evaluation context to the provider's track function similarly to the manner it does in resolvers.
|
||||
|
||||
See: [context levels and merging](./03-evaluation-context.md#32-context-levels-and-merging).
|
||||
|
||||
#### Requirement 6.1.4
|
||||
|
||||
> If the client's `track` function is called and the associated provider does not implement tracking, the client's `track` function **MUST** no-op.
|
||||
|
||||
### 6.2. Tracking Event Details
|
||||
|
||||
The `tracking event details` structure defines optional data pertinent to a particular `tracking event`.
|
||||
|
||||
#### Requirement 6.2.1
|
||||
|
||||
> The `tracking event details` structure **MUST** define an optional numeric `value`, associating a scalar quality with an `tracking event`.
|
||||
|
||||
`Value` is a well-defined field which some providers may map to equivalent numeric values in their API.
|
||||
|
||||
See [provider tracking support](./02-providers.md#27-tracking-support).
|
||||
|
||||
#### Requirement 6.2.2
|
||||
|
||||
> The `tracking event details` **MUST** support the inclusion of custom fields, having keys of type `string`, and values of type `boolean | string | number | structure`.
|
||||
|
||||
The `tracking event details` supports the addition of arbitrary fields, including nested objects, similar to the `evaluation context` and object-typed flag values.
|
||||
|
||||
See [structure](../types.md#structure), [evaluation context](.//03-evaluation-context.md).
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"position": 3,
|
||||
"label": "Sections",
|
||||
"collapsible": false,
|
||||
"link": {
|
||||
"type": "generated-index",
|
||||
"title": "Specification Sections"
|
||||
}
|
||||
}
|
|
@ -1,3 +1,9 @@
|
|||
---
|
||||
title: Types and Data Structures
|
||||
description: A description of types and data structures used within the OpenFeature specification.
|
||||
sidebar_position: 2
|
||||
---
|
||||
|
||||
# Types and Data Structures
|
||||
|
||||
## Overview
|
||||
|
@ -22,31 +28,171 @@ Structured data, presented however is idiomatic in the implementation language,
|
|||
|
||||
### Datetime
|
||||
|
||||
A language primitive for representing a date and time, including timezone information.
|
||||
A language primitive for representing a date and time, optionally including timezone information. If no timezone is specified, the date and time will be treated as UTC.
|
||||
|
||||
### Evaluation Details
|
||||
|
||||
A structure representing the result of the [flag evaluation process](./glossary.md#evaluating-flag-values), and made available in the [detailed flag resolution functions](./flag-evaluation.md#detailed-flag-evaluation), containing the following fields:
|
||||
A structure representing the result of the [flag evaluation process](./glossary.md#evaluating-flag-values), and made available in the [detailed flag resolution functions](./sections/01-flag-evaluation.md#14-detailed-flag-evaluation), containing the following fields:
|
||||
|
||||
- flag key (string, required)
|
||||
- value (boolean | string | number | structure, required)
|
||||
- error code (string, optional)
|
||||
- error code ([error code](#error-code), optional)
|
||||
- error message (string, optional)
|
||||
- reason (string, optional)
|
||||
- variant (string, optional)
|
||||
- flag metadata ([flag metadata](#flag-metadata))
|
||||
|
||||
> [!NOTE]
|
||||
> Some type systems feature useful constraints that can enhance the ergonomics of the `evaluation details` structure.
|
||||
> For example, in the case of an unsuccessful evaluation, `error code`, `reason`, and `error message` will be set, and `variant` will not.
|
||||
> If the type system of the implementation supports the expression of such constraints, consider defining them.
|
||||
|
||||
### Resolution Details
|
||||
|
||||
A structure which contains a subset of the fields defined in the `evaluation details`, representing the result of the provider's [flag resolution process](./glossary.md#resolving-flag-values), including:
|
||||
|
||||
- value (boolean | string | number | structure, required)
|
||||
- error code (string, optional)
|
||||
- error code ([error code](#error-code), optional)
|
||||
- error message (string, optional)
|
||||
- reason (string, optional)
|
||||
- variant (string, optional)
|
||||
- flag metadata ([flag metadata](#flag-metadata), optional)
|
||||
|
||||
\*NOTE: The `resolution details` structure is not exposed to the Application Author. It defines the data which Provider Authors must return when resolving the value of flags.
|
||||
#### Resolution Reason
|
||||
|
||||
A set of pre-defined reasons is enumerated below:
|
||||
|
||||
| Reason | Explanation |
|
||||
| --------------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| STATIC | The resolved value is static (no dynamic evaluation). |
|
||||
| DEFAULT | The resolved value fell back to a pre-configured value (no dynamic evaluation occurred or dynamic evaluation yielded no result). |
|
||||
| TARGETING_MATCH | The resolved value was the result of a dynamic evaluation, such as a rule or specific user-targeting. |
|
||||
| SPLIT | The resolved value was the result of pseudorandom assignment. |
|
||||
| CACHED | The resolved value was retrieved from a cache. |
|
||||
| DISABLED | The resolved value was the result of the flag being disabled in the management system. |
|
||||
| UNKNOWN | The reason for the resolved value could not be determined. |
|
||||
| STALE | The resolved value is non-authoritative or possibly out of date |
|
||||
| ERROR | The resolved value was the result of an error. |
|
||||
|
||||
> [!NOTE]
|
||||
> The `reason` should not be limited to the reasons enumerated above. It can be any of the pre-defined reasons, or
|
||||
> any string value. Some type systems have features which can increase the ergonomics of `reason`, for instance a union
|
||||
> of pre-defined types with a string, or a rust-style enumeration which allows for enumerated values to have associated
|
||||
> content.
|
||||
|
||||
```rust
|
||||
enum Reason {
|
||||
Static,
|
||||
Default,
|
||||
TargetingMatch,
|
||||
Split,
|
||||
Cached,
|
||||
Unknown,
|
||||
Stale,
|
||||
Error,
|
||||
Other(String)
|
||||
}
|
||||
|
||||
let myReason = Reason::Other("my-reason".to_string());
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> The `resolution details` structure is not exposed to the Application Author.
|
||||
> It defines the data which Provider Authors must return when resolving the value of flags.
|
||||
|
||||
### Error Code
|
||||
|
||||
An enumerated error code represented idiomatically in the implementation language.
|
||||
|
||||
| Error Code | Explanation |
|
||||
| --------------------- | ------------------------------------------------------------------------------------------- |
|
||||
| PROVIDER_NOT_READY | The value was resolved before the provider was initialized. |
|
||||
| FLAG_NOT_FOUND | The flag could not be found. |
|
||||
| PARSE_ERROR | An error was encountered parsing data, such as a flag configuration. |
|
||||
| TYPE_MISMATCH | The type of the flag value does not match the expected type. |
|
||||
| TARGETING_KEY_MISSING | The provider requires a targeting key and one was not provided in the `evaluation context`. |
|
||||
| INVALID_CONTEXT | The `evaluation context` does not meet provider requirements. |
|
||||
| PROVIDER_FATAL | The provider has entered an irrecoverable error state. |
|
||||
| GENERAL | The error was for a reason not enumerated above. |
|
||||
|
||||
### Evaluation Context
|
||||
|
||||
See [evaluation context](./sections/03-evaluation-context.md).
|
||||
|
||||
### Evaluation Options
|
||||
|
||||
A structure containing the following fields:
|
||||
|
||||
- hooks (one or more [hooks](./hooks.md), optional)
|
||||
- hooks (one or more [hooks](./sections/04-hooks.md), optional)
|
||||
- hook hints ([hook hints structure](./sections/04-hooks.md#42-hook-hints), optional)
|
||||
|
||||
### Flag Metadata
|
||||
|
||||
A structure which supports definition of arbitrary properties, with keys of type `string`, and values of type `boolean`, `string`, or `number`.
|
||||
|
||||
This structure is populated by a provider for use by an [Application Author](./glossary.md#application-author) via the [Evaluation API](./glossary.md#evaluation-api) or an [Application Integrator](./glossary.md#application-integrator) via [hooks](./sections/04-hooks.md).
|
||||
|
||||
### Provider Status
|
||||
|
||||
An enumeration of possible provider states.
|
||||
|
||||
| Status | Explanation |
|
||||
| ------------ | -------------------------------------------------------------------------------------------------- |
|
||||
| NOT_READY | The provider has not been initialized. |
|
||||
| READY | The provider has been initialized, and is able to reliably resolve flag values. |
|
||||
| ERROR | The provider is initialized but is not able to reliably resolve flag values. |
|
||||
| STALE | The provider's cached state is no longer valid and may not be up-to-date with the source of truth. |
|
||||
| FATAL | The provider has entered an irrecoverable error state. |
|
||||
| RECONCILING* | The provider is reconciling its state with a context change. |
|
||||
|
||||
\* [static context (client-side) paradigm](./glossary.md#static-context-paradigm) only
|
||||
|
||||
### Provider Event Details
|
||||
|
||||
A structure defining a provider event payload, including:
|
||||
|
||||
- flags changed (string[], optional)
|
||||
- message (string, optional)
|
||||
- error code ([error code](#error-code), optional)
|
||||
- event metadata ([event metadata](#event-metadata))
|
||||
|
||||
### Event Details
|
||||
|
||||
A structure defining an event payload, including:
|
||||
|
||||
- provider name (string, required)
|
||||
- flags changed (string[], optional)
|
||||
- message (string, optional)
|
||||
- error code ([error code](#error-code), optional)
|
||||
- event metadata ([event metadata](#event-metadata))
|
||||
|
||||
### Event Metadata
|
||||
|
||||
A structure supporting the addition of arbitrary event data.
|
||||
It supports definition of arbitrary properties, with keys of type `string`, and values of type `boolean`, `string`, or `number`.
|
||||
|
||||
### Provider Events
|
||||
|
||||
An enumeration of provider events.
|
||||
|
||||
| Event | Explanation |
|
||||
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------- |
|
||||
| PROVIDER_READY | The provider is ready to perform flag evaluations. |
|
||||
| PROVIDER_ERROR | The provider signaled an error. |
|
||||
| PROVIDER_CONFIGURATION_CHANGED | A change was made to the backend flag configuration. |
|
||||
| PROVIDER_STALE | The provider's cached state is no longer valid and may not be up-to-date with the source of truth. |
|
||||
| PROVIDER_RECONCILING* | The context associated with the provider has changed, and the provider has not yet reconciled its associated state. |
|
||||
| PROVIDER_CONTEXT_CHANGED* | The context associated with the provider has changed, and the provider has reconciled its associated state. |
|
||||
|
||||
\* [static context (client-side) paradigm](./glossary.md#static-context-paradigm) only
|
||||
|
||||
### Handler Functions
|
||||
|
||||
A function or method which can be associated with a `provider event`, and runs when that event occurs.
|
||||
It declares an `event details` parameter.
|
||||
|
||||
### Tracking Event Details
|
||||
|
||||
A structure which supports definition of arbitrary properties, including nested objects, similar to the `evaluation context` and object-typed flag values.
|
||||
|
||||
See [tracking event details](./sections/06-tracking.md#62-tracking-event-details), [evaluation context](#evaluation-context).
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
FROM cgr.dev/chainguard/python:latest
|
||||
|
||||
WORKDIR .
|
||||
|
||||
COPY ./spec_finder.py ./
|
||||
VOLUME /appdir
|
||||
|
||||
ENTRYPOINT ["python", "spec_finder.py"]
|
|
@ -0,0 +1,49 @@
|
|||
# Repo Parser
|
||||
|
||||
This will parse the contents of an OpenFeature repo and determine how they adhere to the spec. This can be gamed and assumes everyone is participating in good faith.
|
||||
|
||||
We look for a `.specrc` file in the root of a repository to figure out how to find test cases that are annotated with the spec number and the text of the spec. We can then produce a report which says "you're covered" or details about how you're not covered. The goal of this is to use that resulting report to power a spec-compliance matrix for end users to vet SDK quality.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
$ docker build -t specfinder .
|
||||
$ docker run --mount src=/path/tojava-sdk/,target=/appdir,type=bind -it specfinder \
|
||||
spec_finder.py --code-directory /appdir --diff --json-report
|
||||
```
|
||||
|
||||
### `.specrc`
|
||||
|
||||
This should be at the root of the repository.
|
||||
|
||||
`multiline_regex` captures the text which contains the test marker. In java, for instance, it's a specially crafted annotation. `number_subregex` and `text_subregex` which will match the substring found in the `multiline_regex` to parse the spec number and text found. These are multi-line regexes.
|
||||
|
||||
Example:
|
||||
|
||||
```conf
|
||||
[spec]
|
||||
file_extension=java
|
||||
multiline_regex=@Specification\((?P<innards>.*?)\)\s*$
|
||||
number_subregex=number\s*=\s*['"](.*?)['"]
|
||||
text_subregex=text\s*=\s*['"](.*)['"]
|
||||
```
|
||||
|
||||
You can test the regex in python like this to validate they work:
|
||||
|
||||
```
|
||||
$ python3
|
||||
Python 3.9.6 (default, Feb 3 2024, 15:58:27)
|
||||
[Clang 15.0.0 (clang-1500.3.9.4)] on darwin
|
||||
Type "help", "copyright", "credits" or "license" for more information.
|
||||
>>> import re
|
||||
>>> text = ''' @Specification(number="4.3.6", text="The after stage MUST run after flag resolution occurs. It accepts a hook context (required), flag evaluation details (required) and hook hints (optional). It has no return value.")
|
||||
... @Specification(number="4.3.7", text="The error hook MUST run when errors are encountered in the before stage, the after stage or during flag resolution. It accepts hook context (required), exception representing what went wrong (required), and hook hints (optional). It has no return value.")
|
||||
... '''
|
||||
>>> entries = re.findall(r'@Specification\((?P<innards>.*?)\)\s*$', text, re.MULTILINE | re.DOTALL)
|
||||
>>> entries
|
||||
['number="4.3.7", text="The error hook MUST run when errors are encountered in the before stage, the after stage or during flag resolution. It accepts hook context (required), exception representing what went wrong (required), and hook hints (optional). It has no return value."']
|
||||
>>> re.findall(r'''number\s*=\s*['"](.*?)['"]''', entries[0], re.MULTILINE | re.DOTALL)
|
||||
['4.3.7']
|
||||
>>> re.findall(r'''text\s*=\s*['"](.*)['"]''', entries[0], re.MULTILINE | re.DOTALL)
|
||||
['The error hook MUST run when errors are encountered in the before stage, the after stage or during flag resolution. It accepts hook context (required), exception representing what went wrong (required), and hook hints (optional). It has no return value.']
|
||||
```
|
|
@ -0,0 +1,17 @@
|
|||
[tool.mypy]
|
||||
files = "spec_finder.py"
|
||||
local_partial_types = true # will become the new default from version 2
|
||||
pretty = true
|
||||
strict = true
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 120
|
||||
target-version = "py312"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
"I",
|
||||
]
|
||||
|
||||
[tool.ruff.format]
|
||||
quote-style = "single"
|
|
@ -0,0 +1,4 @@
|
|||
mypy
|
||||
pytest
|
||||
pytest-cov
|
||||
ruff
|
|
@ -0,0 +1,215 @@
|
|||
#!/usr/bin/python
|
||||
from __future__ import annotations
|
||||
|
||||
import configparser
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import urllib.request
|
||||
from typing import Any, TypedDict, cast
|
||||
|
||||
|
||||
class Config(TypedDict):
|
||||
file_extension: str
|
||||
multiline_regex: str
|
||||
number_subregex: str
|
||||
text_subregex: str
|
||||
inline_comment_prefix: str | None
|
||||
|
||||
|
||||
Report = TypedDict(
|
||||
'Report',
|
||||
{
|
||||
'extra': list[str],
|
||||
'missing': list[str],
|
||||
'different-text': list[str],
|
||||
'good': list[str],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _demarkdown(t: str) -> str:
|
||||
return t.replace('**', '').replace('`', '').replace('"', '')
|
||||
|
||||
|
||||
def get_spec_parser(code_dir: str) -> Config:
|
||||
with open(os.path.join(code_dir, '.specrc')) as f:
|
||||
data = '\n'.join(f.readlines())
|
||||
|
||||
typical = configparser.ConfigParser(comment_prefixes=())
|
||||
typical.read_string(data)
|
||||
retval = typical['spec']
|
||||
|
||||
if 'inline_comment_prefix' in retval:
|
||||
# If an `inline_comment_prefix` is set, then we're using the inline
|
||||
# comment approach, which should obviate artisnal regexes.
|
||||
retval['multiline_regex'] = r'spec:(.*?):end'
|
||||
retval['number_subregex'] = r'(?P<number>[\d.]+):'
|
||||
retval['text_subregex'] = r'[\d.]+:(.*)'
|
||||
else:
|
||||
assert 'file_extension' in retval
|
||||
assert 'multiline_regex' in retval
|
||||
assert 'number_subregex' in retval
|
||||
assert 'text_subregex' in retval
|
||||
return cast(Config, retval)
|
||||
|
||||
|
||||
def get_spec(force_refresh: bool = False, path_prefix: str = './') -> dict[str, Any]:
|
||||
spec_path = os.path.join(path_prefix, 'specification.json')
|
||||
print('Going to look in ', spec_path)
|
||||
if os.path.exists(spec_path) and not force_refresh:
|
||||
with open(spec_path) as f:
|
||||
data = ''.join(f.readlines())
|
||||
else:
|
||||
# TODO: Status code check
|
||||
spec_response = urllib.request.urlopen(
|
||||
'https://raw.githubusercontent.com/open-feature/spec/main/specification.json'
|
||||
)
|
||||
raw = []
|
||||
for i in spec_response.readlines():
|
||||
raw.append(i.decode('utf-8'))
|
||||
data = ''.join(raw)
|
||||
with open(spec_path, 'w') as f:
|
||||
f.write(data)
|
||||
return cast('dict[str, Any]', json.loads(data))
|
||||
|
||||
|
||||
def specmap_from_file(actual_spec: dict[str, Any]) -> dict[str, str]:
|
||||
spec_map = {}
|
||||
for entry in actual_spec['rules']:
|
||||
if 'requirement' in entry['machine_id']:
|
||||
if number := re.search(r'[\d.]+', entry['id']):
|
||||
spec_map[number.group()] = _demarkdown(entry['content'])
|
||||
else:
|
||||
print(f'Skipping invalid ID {entry["id"]}')
|
||||
|
||||
if entry['children']:
|
||||
for ch in entry['children']:
|
||||
if 'requirement' in ch['machine_id']:
|
||||
if number := re.search(r'[\d.]+', ch['id']):
|
||||
spec_map[number.group()] = _demarkdown(ch['content'])
|
||||
else:
|
||||
print(f'Skipping invalid child ID {ch["id"]}')
|
||||
return spec_map
|
||||
|
||||
|
||||
def find_covered_specs(config: Config, data: str) -> dict[str, str]:
|
||||
repo_specs = {}
|
||||
for match in re.findall(config['multiline_regex'], data, re.MULTILINE | re.DOTALL):
|
||||
match = match.replace('\n', '').replace(config['inline_comment_prefix'], '')
|
||||
# normalize whitespace
|
||||
match = re.sub(' {2,}', ' ', match.strip())
|
||||
number = re.findall(config['number_subregex'], match)[0]
|
||||
|
||||
text_with_concat_chars = re.findall(config['text_subregex'], match, re.MULTILINE | re.DOTALL)
|
||||
try:
|
||||
text = ''.join(text_with_concat_chars).strip()
|
||||
# We have to match for ") to capture text with parens inside, so we add the trailing " back in.
|
||||
text = _demarkdown(eval('"%s"' % text))
|
||||
repo_specs[number] = text
|
||||
except Exception as e:
|
||||
print(f"Skipping {match} b/c we couldn't parse it")
|
||||
return repo_specs
|
||||
|
||||
|
||||
def gen_report(from_spec: dict[str, str], from_repo: dict[str, str]) -> Report:
|
||||
extra = set()
|
||||
different_text = set()
|
||||
good = set()
|
||||
|
||||
missing = set(from_spec.keys()) # assume they're all missing
|
||||
|
||||
for number, text in from_repo.items():
|
||||
if number in missing:
|
||||
missing.remove(number)
|
||||
if number not in from_spec:
|
||||
extra.add(number)
|
||||
continue
|
||||
if text == from_spec[number]:
|
||||
good.add(number)
|
||||
else:
|
||||
different_text.add(number)
|
||||
|
||||
return {
|
||||
'extra': sorted(extra),
|
||||
'missing': sorted(missing),
|
||||
'different-text': sorted(different_text),
|
||||
'good': sorted(good),
|
||||
}
|
||||
|
||||
|
||||
def main(
|
||||
code_directory: str,
|
||||
refresh_spec: bool = False,
|
||||
diff_output: bool = False,
|
||||
limit_numbers: str | None = None,
|
||||
json_report: bool = False,
|
||||
) -> None:
|
||||
actual_spec = get_spec(refresh_spec, path_prefix=code_directory)
|
||||
config = get_spec_parser(code_directory)
|
||||
|
||||
spec_map = specmap_from_file(actual_spec)
|
||||
|
||||
repo_specs: dict[str, str] = {}
|
||||
bad_num = 0
|
||||
|
||||
for root, dirs, files in os.walk('.', topdown=False):
|
||||
for name in files:
|
||||
F = os.path.join(root, name)
|
||||
if ('.%s' % config['file_extension']) not in name:
|
||||
continue
|
||||
with open(F) as f:
|
||||
data = ''.join(f.readlines())
|
||||
|
||||
repo_specs |= find_covered_specs(config, data)
|
||||
|
||||
report = gen_report(from_spec=spec_map, from_repo=repo_specs)
|
||||
|
||||
for number in report['different-text']:
|
||||
bad_num += 1
|
||||
print(f'{number} is bad.')
|
||||
if diff_output:
|
||||
print('Official:')
|
||||
print('\t%s' % spec_map[number])
|
||||
print('')
|
||||
print('Ours:')
|
||||
print('\t%s' % repo_specs[number])
|
||||
|
||||
bad_num += len(report['extra'])
|
||||
for number in report['extra']:
|
||||
print(f"{number} is defined in our tests, but couldn't find it in the spec")
|
||||
|
||||
missing = report['missing']
|
||||
bad_num += len(missing)
|
||||
if missing:
|
||||
print('In the spec, but not in our tests: ')
|
||||
for m in sorted(missing):
|
||||
print(f' {m}: {spec_map[m]}')
|
||||
|
||||
if json_report:
|
||||
report_txt = json.dumps(report, indent=4)
|
||||
loc = os.path.join(code_directory, '%s-report.json' % config['file_extension'])
|
||||
with open(loc, 'w') as f:
|
||||
f.write(report_txt)
|
||||
sys.exit(bad_num)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description='Parse the spec to make sure our tests cover it')
|
||||
parser.add_argument('--refresh-spec', action='store_true', help='Re-downloads the spec')
|
||||
parser.add_argument('--diff-output', action='store_true', help='print the text differences')
|
||||
parser.add_argument('--code-directory', action='store', required=True, help='directory to find code in')
|
||||
parser.add_argument('--json-report', action='store_true', help='Store a json report into ${extension}-report.json')
|
||||
parser.add_argument('specific_numbers', metavar='num', type=str, nargs='*', help='limit this to specific numbers')
|
||||
|
||||
args = parser.parse_args()
|
||||
main(
|
||||
code_directory=args.code_directory,
|
||||
refresh_spec=args.refresh_spec,
|
||||
diff_output=args.diff_output,
|
||||
limit_numbers=args.specific_numbers,
|
||||
json_report=args.json_report,
|
||||
)
|
|
@ -0,0 +1,128 @@
|
|||
from spec_finder import Config, find_covered_specs, gen_report, specmap_from_file
|
||||
|
||||
|
||||
def test_simple_singleline():
|
||||
text = """
|
||||
// spec:4.3.6:The after stage MUST run after flag resolution occurs. It accepts a hook context (required), flag evaluation details (required) and hook hints (optional). It has no return value.:end
|
||||
"""
|
||||
cfg: Config = {
|
||||
'file_extension': 'rust',
|
||||
'multiline_regex': r'spec:(.*):end',
|
||||
'number_subregex': r'(?P<number>[\d.]+):',
|
||||
'text_subregex': r'[\d.]+:(.*)',
|
||||
'inline_comment_prefix': '//',
|
||||
}
|
||||
output = find_covered_specs(cfg, text)
|
||||
assert '4.3.6' in output
|
||||
assert (
|
||||
output['4.3.6']
|
||||
== 'The after stage MUST run after flag resolution occurs. It accepts a hook context (required), flag evaluation details (required) and hook hints (optional). It has no return value.'
|
||||
)
|
||||
|
||||
|
||||
def test_multiline_comment():
|
||||
text = """
|
||||
// spec:4.3.7:The error hook MUST run when errors are encountered in the
|
||||
// before stage, the after stage or during flag resolution. It accepts hook
|
||||
// context (required), exception representing what went wrong (required), and
|
||||
// hook hints (optional). It has no return value.:end
|
||||
"""
|
||||
cfg: Config = {
|
||||
'file_extension': 'rust',
|
||||
'multiline_regex': r'spec:(.*):end',
|
||||
'number_subregex': r'(?P<number>[\d.]+):',
|
||||
'text_subregex': r'[\d.]+:(.*)',
|
||||
'inline_comment_prefix': '//',
|
||||
}
|
||||
output = find_covered_specs(cfg, text)
|
||||
assert '4.3.7' in output
|
||||
assert (
|
||||
output['4.3.7']
|
||||
== """The error hook MUST run when errors are encountered in the before stage, the after stage or during flag resolution. It accepts hook context (required), exception representing what went wrong (required), and hook hints (optional). It has no return value."""
|
||||
)
|
||||
|
||||
|
||||
def test_report():
|
||||
spec = {
|
||||
'1.2.3': 'good text',
|
||||
'2.3.4': 'different text',
|
||||
'3.4.5': 'missing',
|
||||
}
|
||||
|
||||
repo = {
|
||||
'1.2.3': 'good text',
|
||||
'2.3.4': 'it is different',
|
||||
'4.5.6': 'extra',
|
||||
}
|
||||
|
||||
report = gen_report(spec, repo)
|
||||
assert len(report['good']) == 1
|
||||
assert len(report['different-text']) == 1
|
||||
assert len(report['missing']) == 1
|
||||
assert len(report['extra']) == 1
|
||||
|
||||
assert report['good'] == ['1.2.3']
|
||||
assert report['different-text'] == ['2.3.4']
|
||||
assert report['missing'] == ['3.4.5']
|
||||
assert report['extra'] == ['4.5.6']
|
||||
|
||||
|
||||
def test_report_with_found_spec():
|
||||
spec = {
|
||||
'4.3.6': 'good text',
|
||||
}
|
||||
text = """
|
||||
// spec:4.3.6:good text:end
|
||||
"""
|
||||
cfg: Config = {
|
||||
'file_extension': 'rust',
|
||||
'multiline_regex': r'spec:(.*):end',
|
||||
'number_subregex': r'(?P<number>[\d.]+):',
|
||||
'text_subregex': r'[\d.]+:(.*)',
|
||||
'inline_comment_prefix': '//',
|
||||
}
|
||||
output = find_covered_specs(cfg, text)
|
||||
report = gen_report(spec, output)
|
||||
|
||||
assert report['good'] == ['4.3.6']
|
||||
|
||||
|
||||
def test_specmap_from_file():
|
||||
actual_spec = {
|
||||
'rules': [
|
||||
{
|
||||
'id': 'Requirement 1.1.1',
|
||||
'machine_id': 'requirement_1_1_1',
|
||||
'content': 'The `API`, and any state it maintains SHOULD exist as a global singleton, even in cases wherein multiple versions of the `API` are present at runtime.',
|
||||
'RFC 2119 keyword': 'SHOULD',
|
||||
'children': [],
|
||||
},
|
||||
{
|
||||
'id': 'Condition 2.2.2',
|
||||
'machine_id': 'condition_2_2_2',
|
||||
'content': 'The implementing language type system differentiates between strings, numbers, booleans and structures.',
|
||||
'RFC 2119 keyword': None,
|
||||
'children': [
|
||||
{
|
||||
'id': 'Conditional Requirement 2.2.2.1',
|
||||
'machine_id': 'conditional_requirement_2_2_2_1',
|
||||
'content': 'The `feature provider` interface MUST define methods for typed flag resolution, including boolean, numeric, string, and structure.',
|
||||
'RFC 2119 keyword': 'MUST',
|
||||
'children': [],
|
||||
}
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
spec_map = specmap_from_file(actual_spec)
|
||||
|
||||
assert len(spec_map) == 2
|
||||
assert (
|
||||
spec_map['1.1.1']
|
||||
== 'The API, and any state it maintains SHOULD exist as a global singleton, even in cases wherein multiple versions of the API are present at runtime.'
|
||||
)
|
||||
assert (
|
||||
spec_map['2.2.2.1']
|
||||
== 'The feature provider interface MUST define methods for typed flag resolution, including boolean, numeric, string, and structure.'
|
||||
)
|
|
@ -95,7 +95,7 @@ def find_rfc_2119_keyword(content):
|
|||
) is not None:
|
||||
return rfc_2119_keyword_regex
|
||||
|
||||
def parsed_content_to_heirarchy(parsed_content):
|
||||
def parsed_content_to_hierarchy(parsed_content):
|
||||
'Turns a bunch of headline & content pairings into a tree of requirements'
|
||||
content_tree = []
|
||||
headline_stack = []
|
||||
|
@ -181,9 +181,9 @@ def content_tree_to_spec(ct):
|
|||
|
||||
def parse(markdown_file_path):
|
||||
with open(markdown_file_path, "r") as markdown_file:
|
||||
content_finder = re.compile(r'^(?P<level>#+)(?P<headline>[^\n]+)(?P<rest>[^#]*)', re.MULTILINE)
|
||||
content_finder = re.compile(r'^(?P<level>####+)(?P<headline>[^\n]+)\n+?.*?\n+?(?P<rest>>\s[^#?]*)', re.MULTILINE)
|
||||
parsed = content_finder.findall(markdown_file.read())
|
||||
return parsed_content_to_heirarchy(parsed)
|
||||
return parsed_content_to_hierarchy(parsed)
|
||||
|
||||
def write_json_specifications(requirements):
|
||||
for md_absolute_file_path, requirement_sections in requirements.items():
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -4,27 +4,27 @@
|
|||
|
||||
Some content.
|
||||
|
||||
##### Requirement Some requirement
|
||||
#### Requirement Some requirement
|
||||
|
||||
> This **MUST** be done.
|
||||
|
||||
##### Requirement Some other requirement
|
||||
#### Requirement Some other requirement
|
||||
|
||||
> This **MUST NOT** be done.
|
||||
|
||||
##### Requirement Another requirement-name
|
||||
#### Requirement Another requirement-name
|
||||
|
||||
> This **SHOULD** be done in a certain way.
|
||||
|
||||
##### Condition 1
|
||||
#### Condition 1
|
||||
|
||||
> This is a condition.
|
||||
>
|
||||
> ##### Condition 1.1
|
||||
> #### Condition 1.1
|
||||
>
|
||||
> > This is a condition.
|
||||
> >
|
||||
> > ##### Condition 1.1.1
|
||||
> > #### Condition 1.1.1
|
||||
> >
|
||||
> > > This is a condition.
|
||||
> > >
|
||||
|
@ -40,7 +40,7 @@ Some content.
|
|||
> > >
|
||||
> > > > This **MAY** be done.
|
||||
>
|
||||
> ##### Condition 1.2
|
||||
> #### Condition 1.2
|
||||
>
|
||||
> > This is a condition.
|
||||
> >
|
||||
|
@ -50,11 +50,11 @@ Some content.
|
|||
|
||||
Some content.
|
||||
|
||||
##### Requirement This is the name of-the-requirement
|
||||
#### Requirement This is the name of-the-requirement
|
||||
|
||||
> This **MUST NOT** be done.
|
||||
|
||||
##### Condition 2
|
||||
#### Condition 2
|
||||
|
||||
> This is a condition.
|
||||
>
|
||||
|
|
Loading…
Reference in New Issue