├── .github ├── ISSUE_TEMPLATE │ └── Proposal.yaml └── workflows │ ├── ofep-publish.yml │ ├── proposal_autoassign.yml │ └── stale.yml ├── .gitignore ├── .mergify.yml ├── OFEP-template.md ├── OFEP ├── CUE-upstream.md ├── add-dispose.md ├── cloud-native-pattern.md ├── extend-provider-metadata.md ├── flag-change-events.md ├── flag-metadata.md ├── flagd-grpc-sync.md ├── flagd-project-charter.md ├── flagd-sockets.md ├── images │ ├── cloud-native-pattern │ │ ├── architecture.png │ │ ├── config-reload.png │ │ ├── e2e-diagram.png │ │ ├── host-to-agent-com.png │ │ ├── remote-provider.png │ │ └── single-point-of-failure.png │ ├── cue-upstream │ │ └── architecture.png │ ├── flagd-sockets │ │ └── communication.png │ ├── grpc-sync │ │ ├── bidirectional-communication.png │ │ └── dataflow.png │ ├── kubecon-demo │ │ └── architecture.png │ ├── kubernetes-sync-service │ │ ├── communication.png │ │ └── notification.png │ └── ofo-client-support │ │ └── architecture.png ├── inline-evaluation.md ├── kubecon-demo.md ├── kubernetes-sync-service.md ├── metric-hooks.md ├── ofo-flag-service.md ├── ofo-flagd-client-support.md ├── provider-client-mapping.md ├── provider-hook.md ├── provider-metadata-capability-discovery.md ├── sdk-e2e-test-strategy.md ├── sdk-wait-provider-ready.md ├── single-context-paradigm.md └── transaction-context-propagation.md ├── README.md ├── docs ├── Makefile ├── copy_files.py ├── generate_index.py ├── requirements.txt └── source │ └── conf.py └── prior-art ├── api-comparision.md └── existing-landscape.md /.github/ISSUE_TEMPLATE/Proposal.yaml: -------------------------------------------------------------------------------- 1 | name: Enhancement Proposal 2 | description: Submit a proposal for a new enhancement. 3 | title: "[Proposal] " 4 | labels: ["OFEP"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Hi there! Thanks for your interest in submitting a proposal to OpenFeature Enhancement Proposal (OFEP)! We are excited to review your ideas and work with you. We appreciate your time and effort in helping us make our community-driven platform even better. 10 | Before filling out the form, please take a moment to review our [Should I create an OFEP?](https://github.com/open-feature/ofep#should-i-create-an-ofep) section. 11 | As you draft your proposal, please keep in mind that we value clear and concise language, please make sure to follow common issue etiquette like: 12 | * title describes the content accurately. 13 | * sentences are short and clear. 14 | * grammar and spelling errors are not distorting the meaning. 15 | - type: dropdown 16 | id: category 17 | attributes: 18 | label: Category of Proposal 19 | description: What category does your proposal best fit? This helps us better identify your proposal to determine who to engage with on our side. 20 | options: 21 | - Specification 22 | - OpenFeature Operator 23 | - Flagd 24 | - SDKs 25 | - Other 26 | validations: 27 | required: true 28 | - type: textarea 29 | id: overview 30 | attributes: 31 | label: Describe your proposal 32 | description: Please provide us with a summary of your proposal and explain how it aligns with OpenFeature’s mission. 33 | validations: 34 | required: true 35 | 36 | -------------------------------------------------------------------------------- /.github/workflows/ofep-publish.yml: -------------------------------------------------------------------------------- 1 | name: "OFEP Doc Publish" 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - name: Build HTML 14 | uses: ammaraskar/sphinx-action@master 15 | - name: Upload artifacts 16 | uses: actions/upload-pages-artifact@v2 17 | with: 18 | name: github-pages 19 | path: docs/build/html/ 20 | deploy: 21 | needs: build 22 | 23 | permissions: 24 | pages: write # to deploy to Pages 25 | id-token: write # to verify the deployment originates from an appropriate source 26 | 27 | environment: 28 | name: github-pages 29 | url: ${{ steps.deployment.outputs.page_url }} 30 | 31 | runs-on: ubuntu-latest 32 | steps: 33 | - name: Deploy to GitHub Pages 34 | id: deployment 35 | uses: actions/deploy-pages@v2 -------------------------------------------------------------------------------- /.github/workflows/proposal_autoassign.yml: -------------------------------------------------------------------------------- 1 | name: Issue assignment 2 | 3 | on: 4 | issues: 5 | types: [opened,edited] 6 | 7 | jobs: 8 | auto-assign: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | issues: write 12 | steps: 13 | - name: Check if issue has [Proposal] label 14 | id: check_proposal_label 15 | run: | 16 | echo "has_proposal_label=${{ contains(github.event.issue.title, 'proposal') }}" >> "$GITHUB_OUTPUT" 17 | shell: bash 18 | 19 | - if: ${{steps.check_proposal_label.outputs.has_proposal_label == 'true'}} 20 | name: Run auto-assignment script 21 | id: check_proposal_label_1 22 | run: | 23 | category3=$(echo "${{ github.event.issue.body }}" | grep -e "Category" -A2 | grep -v "Category" | grep "\S" | tr -d " \t\n\r" | tr -d '"') 24 | echo "category=$category3" >> "$GITHUB_OUTPUT" 25 | shell: bash 26 | 27 | - if: ${{ (steps.check_proposal_label_1.outputs.category == 'SDKs') || (steps.check_proposal_label_1.outputs.category == 'Specification')}} 28 | name: 'Auto-assign Spec and SDK Maintainers' 29 | uses: pozil/auto-assign-issue@v1 30 | with: 31 | teams: | 32 | sdk-dotnet-maintainers 33 | sdk-golang-maintainers 34 | sdk-java-maintainers 35 | sdk-javascript-maintainers 36 | sdk-python-maintainers 37 | sdk-ruby-maintainers 38 | sdk-rust-maintainers 39 | sdk-php-maintainers 40 | numOfAssignee: 10 41 | removePreviousAssignees: false 42 | allowSelfAssign: true 43 | repo-token: ${{ secrets.ASSIGN_TEAM }} 44 | 45 | - if: ${{ (steps.check_proposal_label_1.outputs.category == 'Flagd') || (steps.check_proposal_label_1.outputs.category == 'OpenFeatureOperator')}} 46 | name: 'Auto-assign Cloud Native Maintainers' 47 | uses: pozil/auto-assign-issue@v1 48 | with: 49 | teams: cloud-native-maintainers 50 | numOfAssignee: 5 51 | removePreviousAssignees: false 52 | allowSelfAssign: true 53 | repo-token: ${{ secrets.ASSIGN_TEAM }} 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Stale Issue/PR Reminder 2 | 3 | on: 4 | schedule: 5 | - cron: '0 12 * * *' # Run every day at 12:00 UTC 6 | 7 | jobs: 8 | stale-reminder: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout code 12 | uses: actions/checkout@v2 13 | 14 | - name: Determine staleness 15 | uses: actions/stale@v5 16 | with: 17 | 18 | stale-pr-message: 'This PR is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in next 60 days.' 19 | close-pr-message: 'This PR was closed automatically because there has not been any activity for 90 days. You can reopen the PR if you would like to continue to work on it.' 20 | 21 | stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in next 60 days.' 22 | close-issue-message: 'This issue was closed automatically because there has not been any activity for 90 days. You can reopen the issue if you would like to continue to work on it.' 23 | 24 | days-before-stale: 30 25 | days-before-close: 90 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | docs/build/** 2 | docs/source/** 3 | 4 | # Don't ingore the configurate file 5 | !docs/source/conf.py -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | pull_request_rules: 2 | - name: Detect and label OFEPs eligble to be automatically merged 3 | conditions: 4 | - "#approved-reviews-by>=1" 5 | - "#changes-requested-reviews-by=0" 6 | - "#review-threads-unresolved=0" 7 | - "check-success=Test added-OFEP proposals for status" 8 | actions: 9 | comment: 10 | message: "Hello :wave: @{{author}}, this OFEP is eligible to be merged. It will be automatically merged after 3 working days if no objections are raised." 11 | label: 12 | add: 13 | - automerge 14 | - name: Remove the label 15 | conditions: 16 | - or: 17 | - "#approved-reviews-by=0" 18 | - "#changes-requested-reviews-by>=1" 19 | - "#review-threads-unresolved>=1" 20 | - label=automerge 21 | actions: 22 | label: 23 | remove: 24 | - automerge 25 | - name: Merging an approved OFEP on Thur or Fri after 3 working days 26 | conditions: 27 | - "#approved-reviews-by>=1" 28 | - "#changes-requested-reviews-by=0" 29 | - "#review-threads-unresolved=0" 30 | - label=automerge 31 | - updated-at<3 days ago 32 | - schedule=Thu-Fri 09:00-19:00[America/Vancouver] 33 | actions: 34 | merge: 35 | method: squash 36 | commit_message_template: "@{{title}}" 37 | - name: Merging an approved OFEP on Mon, Tue, or Wed after 3 working days 38 | conditions: 39 | - "#approved-reviews-by>=1" 40 | - "#changes-requested-reviews-by=0" 41 | - "#review-threads-unresolved=0" 42 | - label=automerge 43 | - updated-at<5 days ago 44 | - schedule=Mon-Wed 09:00-19:00[America/Vancouver] 45 | actions: 46 | merge: 47 | method: squash 48 | commit_message_template: "@{{title}}" 49 | -------------------------------------------------------------------------------- /OFEP-template.md: -------------------------------------------------------------------------------- 1 | --- 2 | date: YYYY-MM-DD 3 | title: OFEP Template 4 | 7 | status: Approved 8 | authors: [] 9 | tags: [] 10 | --- 11 | 12 | # Name 13 | 14 | ## State: ( DRAFTING | WITHDRAWN | PENDING REVIEW | APPROVED | REJECTED ) 15 | 16 | The OFEP begins with a brief overview. This section should be one or two paragraphs that just explains what the goal of this OFEP is going to be, but without diving too deeply into the "why", "why now", "how", etc. Ensure anyone opening the document will form a clear understanding of the OFEP intent from reading this paragraph(s). 17 | 18 | ## Background 19 | 20 | The next section is the "Background" section. This section should be at least two paragraphs and can take up to a whole page in some cases. The guiding goal of the background section is: as a newcomer to this project (new employee, team transfer), can I read the background section and follow any links to get the full context of why this change is necessary? 21 | 22 | If you can't show a random engineer the background section and have them acquire nearly full context on the necessity for the RFC, then the background section is not full enough. To help achieve this, link to prior RFCs, discussions, and more here as necessary to provide context so you don't have to simply repeat yourself. 23 | 24 | ## Proposal 25 | 26 | The next required section is "Proposal" or "Goal". Given the background above, this section proposes a solution. This should be an overview of the "how" for the solution, but for details further sections will be used. 27 | 28 | ## Sections 29 | 30 | From this point onwards, the sections and headers are generally freeform depending on the OFEP. Sections are styled as "Heading 2". Try to organize your information into self-contained sections that answer some critical question, and organize your sections into an order that builds up knowledge necessary (rather than forcing a reader to jump around to gain context). 31 | 32 | Sections often are split further into sub-sections styled "Heading 3". These sub-sections just further help to organize data to ease reading and discussion. 33 | 34 | ### [Example] Implementation 35 | 36 | Many OFEPs have an "implementation" section which details how the implementation will work. This section should explain the rough API changes (internal and external), package changes, etc. The goal is to give an idea to reviews about the subsystems that require change and the surface area of those changes. 37 | 38 | This knowledge can result in recommendations for alternate approaches that perhaps are idiomatic to the project or result in less packages touched. Or, it may result in the realization that the proposed solution in this OFEP is too complex given the problem. 39 | 40 | For the OFEP author, typing out the implementation in a high-level often serves as "rubber duck debugging" and you can catch a lot of issues or unknown unknowns prior to writing any real code. 41 | -------------------------------------------------------------------------------- /OFEP/CUE-upstream.md: -------------------------------------------------------------------------------- 1 | --- 2 | date: 2022-07-12 3 | title: CUE Upstream 4 | status: Draft 5 | authors: [Alex Jones] 6 | tags: [flagd, spec] 7 | 8 | --- 9 | # CUE upstream 10 | 11 | ## State: DRAFTING 12 | 13 | As flagD evolves, it will be presented on different types of services with a variety of protocols. 14 | Many of these protocols have client libraries that can be generated for convenience. 15 | For example, we use openapi to enable us to create a golang server implementation. 16 | This is great because a) it's fast and easy b) it matches the specification files promises exactly - validation/matching for free. 17 | 18 | Given we are looking to now expand to offer new generated server support for gRPC, we are in a predicament about whether to a) derive the implementation from OpenAPI b) have a new source of truth as .proto files c) have some sort of composite between the two. 19 | 20 | My proposal here would be to choose a new top-level DSL to define the specifications required to drive generated server code implementations. As such, cue offers integration with YAML/JSON & protobuf directly. 21 | This would mean we could use a single file(s) and toolchain to generate all of the automatically created code for flagD. 22 | The benefit here would be a single source of truth, ease of contribution, ease of extension and simplified build process. 23 | 24 | ![architecture](images/cue-upstream/architecture.png "architecture") 25 | 26 | 27 | 28 | By switching to CUE, we can use a single build chain to produce the OpenAPI spec as a generated file that is convenient to users who want to build against flagD ( possible code-generating their own client libraries). Though we would not need to use the generated OpenAPI spec through any further generator. Instead, the protoc-http tool will allow for generation from protobuf files as per @James-Milligan initial investigation. 29 | 30 | Next steps: 31 | If we have agreement on this, I would propose looking at a top level CUE file(s) to replace the current OpenAPI YAML file that is generated from. At this point we will be able to prove it can become the new top-of-chain build step, enabling us to then replace the downstream components. -------------------------------------------------------------------------------- /OFEP/add-dispose.md: -------------------------------------------------------------------------------- 1 | --- 2 | date: 2023-05-19 3 | title: Dispose Functionality To API 4 | status: Approved 5 | authors: [Todd Baert, Weyert de Boer] 6 | tags: [api, sdk] 7 | 8 | --- 9 | # Add dispose functionality to API 10 | 11 | ## State: APPROVED 12 | 13 | The goal of this OFEP is to enhance the OpenFeature API to allow authors to cleanly dispose of any resources consumed by OpenFeature providers. 14 | 15 | ## Background 16 | 17 | When implementing OpenFeature providers, you sometimes need to register event handlers, or timers or similar functionality to 18 | correctly handle functionality of the vendor library. Some vendor libraries might also have analytics, or debugging functionality 19 | that can be enabled and might get flushed to the vendor platform when the library gets stopped or disposed off. 20 | 21 | Currently, you aren't able to cleanly handle these use cases in OpenFeature providers. 22 | 23 | ## Proposal 24 | 25 | To allow clean disposal of resources consumed by OpenFeature providers, I would like to propose a function that can be called by OpenFeature SDK that allow to cleanup these resources. 26 | The OpenFeature provider will implement a similar function that will be called internally by the OpenFeature SDK. 27 | The function may be synchronous or asynchronous, as SDK practicalities and language idioms dictate. 28 | In order to facilitate resource disposal paradigms of the implementing language, the precise name of the function won't be specified. 29 | 30 | ### Example Implementation 31 | 32 | The `Provider` interfaces gets extended (in a non-breaking way) to include the following function: 33 | 34 | ```typescript 35 | dispose(): Promise 36 | ``` 37 | 38 | The function can be called by the the `API` instance when it requests the resources that need to be disposed off, for this reason, 39 | the global API needs to be enhanced to also have the `dispose`-function: 40 | 41 | ```typescript 42 | dispose(): Promise 43 | ``` 44 | 45 | A potential implementation could be the following in the Node SDK: 46 | 47 | ```typescript 48 | async dispose(): Promise { 49 | await this.provider.dispose() 50 | } 51 | ``` 52 | -------------------------------------------------------------------------------- /OFEP/cloud-native-pattern.md: -------------------------------------------------------------------------------- 1 | --- 2 | date: 2022-07-12 3 | title: Cloud Native Pattern 4 | status: Approved 5 | authors: [Alex Jones] 6 | tags: [cloud-native] 7 | 8 | --- 9 | 10 | # Cloud native pattern 11 | 12 | ## State: APPROVED 13 | 14 | Hello folks, 15 | 16 | I wanted to share some thoughts on an initial architectural design for the open feature project from the Kubernetes layer. 17 | From my initial engagement and involvement with the group, this will be primarily focused on the server-side capability of presenting feature flags to container workloads. 18 | 19 | ## Assumptions 20 | 21 | We initially assume that workloads will be some sort of web server, however, I would like to incorporate into the design the support of local AF_UNIX sockets for processes to manage their control flow based on external flags. To that end, we are also basing much of our overarching narrative on the ability to perform some sort of A/B testing; at this point not considering the use of flags to manage logic based on some sort of event activity - horizontal pod autoscaling as an example. 22 | 23 | In my following example and illustrations, I have created the latter example of a flask based web server that is presented on the public internet. The flag system however is server-side and should not be programmable from the world wide web. At this time to me, it seems illogical to need to worry about RBAC/ACL and TLS concerns when making a highly sensitive API like this available to the public. 24 | 25 | ## Operator pattern 26 | 27 | The typical Operator pattern in this context is appropriate over a stand-alone web service for the previously mentioned reasons but also because of the stateless and scalable nature of the design. For example; a multi-tenant environment is constrained by namespacing and service accounts; something which we would have to overcome if writing a centralised cluster-wide API. 28 | 29 | In addition to this, there is a "feel" factor in using this pattern as it is easy to design, manage and augment. 30 | I suggest that in conjunction with reconciliation on a primary custom resource, there is also the use of a mutating and validating admission webhook to enable key functionality. 31 | 32 | ## Workflow 33 | 34 | In a nutshell, there are two types of a control flow for setting a feature flag. 35 | Direct set and standing orders are the pet names for them in this illustration. 36 | 37 | These combined should cover the majority of use cases and get the project into a rapidly usable state given they are fairly well-trodden patterns for interacting with services. 38 | 39 | ![architecture](images/cloud-native-pattern/architecture.png "architecture") 40 | 41 | ## Direct set 42 | 43 | This pattern uses labels/annotations ( I am undecided currently as there are penalties on the size of the YAML file for storing large histories here ) directly for the open feature labelling system or a reference to an object that does; for example: 44 | 45 | openfeature.io/standingorder: custom-resource-1 46 | The key point here is that the deployment encapsulates where it wants to derive its feature flags from - this makes the issue of the owner referencing and mapping the configuration to the agent trivial. 47 | 48 | Once a deployment has been configured with the appropriate annotations/labels then a cluster with OpenFeature Operator running will employ an optional namespace scoped set of webhooks. 49 | 50 | validating admission webhook 51 | The purpose of this webhook is to validate any inline JSON that has been configured with a manifest and to provide any additional context that needs to be encoded. This might well be appropriate in a multi-tenant environment to write remarks from an authoritative source. 52 | 53 | For example: 54 | 55 | openfeature.io/operator-remarks: namespaced, scoped, refreshable 56 | mutating admission webhook 57 | The job of this webhook is to run after the validating admission component and inject the OpenFeature agent into the configuration object and complete the required setup to present it to the host container ( the container running the desired workload for feature flagging ) within the pod. 58 | 59 | This webhook will deal with configuration such as open port, transport type and configuration path locations ( possibly expanding to backing type such as PVC vs configmap ). 60 | 61 | ### Configuration reloading 62 | 63 | ![config reload architecture](images/cloud-native-pattern/config-reload.png "uconfig reload architecture") 64 | 65 | In the scenario of a feature flag being altered, the configuration would be modified directly by the controller-manager and the agent would micro reload to present to the host container ( perhaps using the confd workflow ). 66 | 67 | ## Standing orders 68 | 69 | This flow is supplementary to writing annotation directly to the deployment and can coexist within the same ecosystem. 70 | The idea here is that you have a custom resource that can be programmable but might not necessarily be immediately associated with an application. It would be possible for an OpenFeature agent to fetch from a standing order custom resource rather than it's locally scoped configmap in this scenario. 71 | 72 | This is important as it gives a known and persistent custom resource for programming feature flag states but also encourages a subscribe mechanic from a deployment. It might look like the following: 73 | 74 | openfeature.agent/standing-orders-resource: custom-resource-one" 75 | openfeature.agent/standing-orders-resource-namespace: default" 76 | An argument against an API server 77 | I believe there was an initial idea to create a cluster-wide API server, which I would discourage. 78 | The problems presented with RBAC/ACL/TLS and other features are not-intractable but they are secondary to wider architectural design issues. I have laid out a few here. 79 | 80 | ## High-scale stateful configuration management 81 | 82 | Because having state kept in memory is just a bad idea, especially with many engineers attempting to program against the backend API for OpenFeature, my thinking is we would persist this into state files. 83 | 84 | In a large cluster with a centralised server, the thinking is that the configuration state files would need to be persisted to disk, this isn't alone a problem but a single file would create a complex nested object construct which would scale inversely to the number of workloads using open feature flags. This could be compensated by distributing to a state file per workload but at the expense of complexity. Managing orphaned files and alternative formats then compound this issue. 85 | 86 | ## Security 87 | 88 | All workloads requiring access to a centralised server will need both access to the kubernetes control plane, the API server and the host overlay network. There are also risks with turning off someone else's feature flags as discussed which means a lot of machinery around security needs to be built and managed here - how do we check service accounts are valid? How do we check the authority level? How do we map a service account to feature flag permissions? 89 | 90 | As you can see this design is an invitation to reinvent the wheel. 91 | 92 | ## Performance 93 | 94 | Network calls will increase as workloads increase. 95 | Neither design ( operator vs api ) are immune to this, however, the distance of calls will be increasing across nodes on the host overlay network unless there is a separate tenant network for calls to the OpenFeature API server. 96 | 97 | In addition, when the API server fails or restarts all calls will start timing out to it unless there is behaviour introduced into the agents ( which is completely possible ). However, the remark about a single point of failure holds true. 98 | 99 | ![single point of failure](images/cloud-native-pattern/single-point-of-failure.png "single point of failure") 100 | 101 | Let me know your thoughts 102 | 103 | ## Additional architecture 104 | 105 | ![E2E Diagram](images/cloud-native-pattern/e2e-diagram.png "E2E Diagram") 106 | 107 | ## Post Kubecon configuration 108 | 109 | We had a meet up at Kubecon that touched on a few key issues that have helped to improve and inform this design pattern. 110 | 111 | ### remote endpoint configuration 112 | 113 | Given that we want to accommodate vendors and enable them within this ecosystem, we are going to introduce a concept that allows for the Flag Custom Resource to indicate the desire for a remote endpoint point. To that end, it will enable a completely new set of capabilities from the host vendor to interact at the pod level with processes for the cloud-native provider. It serves as a mechanism to instigate a remote fetch capability that would merge or override the local configuration within the custom resource. 114 | 115 | It could possibly have some of these types of fields: 116 | ``` 117 | remoteFlagProvider: 118 | type: 119 | strategy: merge 120 | credentials: 121 | secret: 122 | Agent 123 | ``` 124 | 125 | ### Integration points 126 | 127 | In order to enable host containers to consume the sidecar then there should be multiple protocols to do so. 128 | There was an initial proposal to incorporate the AF_LOCAL/AF_UNIX socket family and within that family, we should decide whether is a need to support SOCK_STREAM and SOCK_DGRAM, I would initially suggest only supporting SOCK_STREAM. 129 | This would enable us to further layer HTTP protocol support on top where required. 130 | 131 | ![Host to Agent Communication](images/cloud-native-pattern/host-to-agent-com.png "Host to Agent Communication") 132 | 133 | ## Flow 134 | 135 | The below illustration has been updated also to reflect the current thinking around the initialisation flow of the flagging system. 136 | 137 | That said there are some learnings from Istio and concerns around side car overhead - namely around upgrading and maintenance. As such it is worth exploring a pattern for rolling or upgrading sidecars, as the implication is that this will force a deployment rollout due to the change on the deployment object ( and other resource types sts/ds) 138 | 139 | ![Remove Provider](images/cloud-native-pattern/remote-provider.png "Remove Provider") 140 | -------------------------------------------------------------------------------- /OFEP/extend-provider-metadata.md: -------------------------------------------------------------------------------- 1 | --- 2 | date: 2023-09-06 3 | title: Extend Provider Metadata 4 | status: Approved 5 | authors: [Michael Beemer] 6 | tags: [spec] 7 | 8 | --- 9 | # Extend provider metadata 10 | 11 | ## State: APPROVED 12 | 13 | This proposal lays out a mechanism for flag providers to surface arbitrary metadata about themselves to OpenFeature, and for hooks to access this metadata. 14 | It's similar in concept to flag metadata but works at the provider level. 15 | This is an ideal location to store information about the provider that's common across all flag evaluations. 16 | 17 | ## Background 18 | 19 | OpenFeature supports [flag metadata](./007-OFEP-provider-flag-metadata.md) which allows for arbitrary information to be associated with a flag evaluation. 20 | This works well for data that specific to an individual flag that was successfully evaluated. 21 | However, it's useful to access arbitrary provider information outside of a successful flag evaluation. 22 | 23 | In many flag management tools, there are ways to group related flag configurations. 24 | A non-exhaustive list includes `organization`, `namespace`, `project`, and `environment`. 25 | As an example, a `flag key` may have different configurations per `environment`. 26 | OpenFeature needs a way to capture provider specific metadata so that it can be leveraged by hooks. 27 | This will allow hooks to provide better troubleshooting and telemetry support. 28 | 29 | As with flag metadata, different providers will expose different kinds of metadata, and different Application Integrators will want to consume that metadata in different ways. 30 | As such, the goal of this proposal is to provide a simple way for the OpenFeature runtime to pass generic metadata from a provider to a hook, without OpenFeature itself understanding the semantics of that metadata. 31 | 32 | ## Non-goals 33 | 34 | In the future we may wish to standardize the semantics of some key metadata attributes which are common amongst many providers (e.g. `organization`, `project`, `environment`) so that provider-agnostic hooks can be created to consume these standard attributes, but that is explicitly NOT in the scope of this OFEP - we would prefer to see which common attributes naturally emerge and then "pave those cow paths". 35 | 36 | ## Proposal 37 | 38 | Extend the [provider metadata](https://openfeature.dev/specification/sections/providers#requirement-211) to allow for a bag of immutable metadata attributes related to the provider itself. 39 | This bag will be a set of key-value pairs, where the key will be a string and the value will be a small set of primitive values. 40 | 41 | `ProviderMetadata` is already defined in the spec. 42 | It currently has a single readonly property `name`. 43 | This requirement would remain, but the `ProviderMetadata` interface would be extended to allow for a type like `Record` in typescript, where the key is the metadata attribute and the value is the metadata value. 44 | These values would be mutable by the provider only. 45 | 46 | ### Code examples 47 | 48 | ```ts 49 | interface ProviderMetadata extends Metadata { 50 | readonly name: string; 51 | } 52 | 53 | interface Metadata { 54 | [key: string]: string | boolean | number; 55 | } 56 | ``` 57 | 58 | ```java 59 | public interface Metadata { 60 | String getName(); 61 | // Add attributes accessor 62 | Map getAttributes(); 63 | } 64 | ``` 65 | 66 | > Since evaluation context is already available to hooks, they already have access to the [provider metadata](https://openfeature.dev/specification/sections/hooks#requirement-412). 67 | -------------------------------------------------------------------------------- /OFEP/flag-change-events.md: -------------------------------------------------------------------------------- 1 | --- 2 | date: 2023-03-14 3 | title: Flag Change Events 4 | status: Approved 5 | authors: [Michael Beemer, Todd Baert, Justin Abrahms] 6 | tags: [flagd, sdk] 7 | 8 | --- 9 | # Flag change events 10 | 11 | ## State: APPROVED 12 | 13 | Some flag SDKs support listening for flag value changes or general configuration changes ([launchdarkly](https://docs.launchdarkly.com/sdk/features/flag-changes), [cloudbees](https://docs.cloudbees.com/docs/cloudbees-feature-management/latest/reporting/configuration-fetched-handler), flagd). This can allow us to use an event-based paradigm for consuming flags. Client apps may use feature flags for characteristics that aren't specifically tied to a user-action, making imperative flag evaluation a less-than-ideal solution. Web clients using frameworks such as React need a way to register a callback for when a provider has initialized, given that it's not uncommon for a flag to be evaluated before the provider is ready (during page load). A server application could listen to a flag that changes some operational behavior. 14 | 15 | Examples: 16 | 17 | - A web client's banner color could be updated when an associated flag is changed, without any user action or a page reload. 18 | - A service could subscribe to a flag that controls its global log-level. 19 | - A subsystem could be restarted with the new flag value. 20 | 21 | ## Design 22 | 23 | The provider interface and the OpenFeature client would be extended to have new functionality to register handlers for a set of events defined by the SDK (`ProviderEvents`). When a consumer (_application integrator_, _application author_, _integration author_) registers a handler on a client, the client maintains this handler. The provider emits events or runs a callback indicating something happened, optionally providing data associated with that event. The callbacks registered with the client are then invoked with this data (`EventData`). 24 | 25 | In the case of the aforementioned flag systems, it is the consumer's responsibility to evaluate any flags in response to the change - flag evaluation would not be automatically performed by the SDK. One reason is that no dynamic context can be reasonably provided in the case of events, since the event is driven by a change in the flag management system, not a user-action. In these cases, the `EventData` would not contain flag values and the application author would have to evaluate flags in the registered handler. This is consistent with the event APIs already existing in those systems. 26 | 27 | How it looks implemented in a Provider: 28 | 29 | ```ts 30 | import { Provider, EventingProvider, ProviderEvents } from './types'; 31 | 32 | class MyEventingProvider implements Provider, EventingProvider { 33 | readonly metadata = { 34 | name: 'My Eventing Provider', 35 | } as const; 36 | 37 | // ... 38 | 39 | // the SDK listens for events, and fires associated handlers the application-author adds. 40 | readonly events = new EventEmitter(); 41 | 42 | // pollDataSource a conceptual method specific to this provider that fires a callback if the flag source-of-truth of this provider changes. 43 | this.pollDataSource((newFlagData) => this.events.emit(ProviderEvents.ConfigurationChanged, newFlagData)) 44 | 45 | // ... 46 | 47 | ``` 48 | 49 | How it looks for an `application author`: 50 | 51 | ```ts 52 | OpenFeature.setProvider(new MyEventingProvider()); 53 | 54 | const client = OpenFeature.getClient(); 55 | 56 | // subscribe to ProviderEvents.ConfigurationChanged events 57 | client.addHandler(ProviderEvents.ConfigurationChanged, (eventData: EventData | undefined) => { 58 | 59 | // see EventData below 60 | if (eventData.flagKeysChanged.contains('myFlagd')) { 61 | onMyFlagChange(); 62 | } 63 | onAnyChange(); 64 | }); 65 | ``` 66 | 67 | Hypothetical enumeration of events: 68 | 69 | ```ts 70 | export enum ProviderEvents { 71 | Ready = 'PROVIDER_READY', 72 | Error = 'PROVIDER_ERROR', 73 | ConfigurationChanged = 'PROVIDER_CONFIGURATION_CHANGED', 74 | Shutdown = 'PROVIDER_SHUTDOWN', 75 | }; 76 | ``` 77 | 78 | Hypothetical structure of `EventData`: 79 | 80 | ```ts 81 | export interface EventData { 82 | flagKeysChanged?: string[], 83 | changeMetadata?: { [key: string]: boolean | string } // similar to flag metadata 84 | } 85 | ``` 86 | 87 | ## Benefits 88 | 89 | Ability to use event-based flag evaluation paradigms; attaching event handlers for specific occurrences. This is particularly useful for reacting to configuration changes, provider readiness, and errors. Such event-based models are particularly useful for web frameworks such as React and Angular. 90 | 91 | ## Caveats 92 | 93 | - Not all providers can reasonably implement this... some SDKs don't support subscriptions, for example. Should these providers simply never fire events? I've attempted to partially address this with: https://github.com/open-feature/ofep/pull/36 94 | - We may need a way to "shutdown" providers, cancelling all handlers - this is addressed by: https://github.com/open-feature/ofep/pull/30 95 | 96 | ## Demo 97 | 98 | [Proposed JS-SDK implementation](https://github.com/open-feature/js-sdk/pull/316) 99 | [Proposed usage in flagd-web provider](https://github.com/open-feature/js-sdk-contrib/pull/142) 100 | -------------------------------------------------------------------------------- /OFEP/flag-metadata.md: -------------------------------------------------------------------------------- 1 | --- 2 | date: 2022-11-17 3 | title: Surfacing Flag Metadata 4 | status: Approved 5 | authors: [Pete Hodgson] 6 | tags: [spec] 7 | 8 | --- 9 | # Surfacing flag metadata 10 | 11 | ## State: APPROVED 12 | This proposal lays out a mechanism for flag providers to surface arbitrary flag metadata to Open Feature, and for hooks to access this metadata. 13 | 14 | 15 | ## Background 16 | 17 | Flag providers maintain metadata about a feature flag, and there are scenarios where a hook might benefit from being able to access this metadata. For example, a flag provider could surface a `management-url` attribute which an open-telemetry hook could then add to an otel span, allowing someone viewing a trace for a feature-flagged operation to easily navigate to a webpage describing that flag in detail. 18 | 19 | Different providers will expose different kinds of metadata, and different Application Integrators will want to consume that metadata in different ways. As such, the goal of this proposal is to provide a simple way for the Open Feature runtime to pass generic metadata from a provider to a hook, without Open Feature itself understanding the semantics of that metadata. 20 | 21 | ## Non-goals 22 | 23 | In the future we may wish to standardize the semantics of some key metadata attributes which are common amongst many providers (e.g. `expiration-date`, `owner`, `management-url`) so that provider-agnostic hooks can be created to consume these standard attributes, but that is explicitly NOT in the scope of this OFEP - we would prefer to see which common attributes naturally emerge and then "pave those cow paths". 24 | 25 | ## Proposal 26 | 27 | The Open Feature spec will be extended in two ways: 28 | - a flag provider will be able to provide a bag of metadata attributes for a feature flag. This bag will be a set of key-value pairs, where the key will be a string and the value will be a small set of primitive values. 29 | - a hook will be able to access these metadata attributes 30 | 31 | *note* what follows is a strawman proposal for one way to extend the spec. Please poke holes! 32 | 33 | We add an optional `flagMetadata:FlagMetadata` field to the Resolution Details structure - and therefore also to the Evaluation Details structure, since the former is a specified as having a subset of the fields in the latter. 34 | 35 | The `FlagMetadata` has a type like `Record` in typescript, where the key is the metadata attribute and the value is the metadata value. 36 | 37 | The flagMetadata field MAY return an empty record. A missing `flagMetadata` field MUST be interpreted as an empty record. 38 | 39 | Since hooks are provided the evaluation context, they would have access to any flag metadata that the provider provides. 40 | 41 | ### other details 42 | 43 | The flagMetadata field should be considered immutable. We won't support adding/removing/editing metadata in hooks, for example. 44 | 45 | Format of the metadata attribute is left up to the provider. Maybe we provide suggestions around formatting, and a namespacing prefix (e.g. `"flags-r-us.management-url"`). Following otel conventions for span attributes might be smart. 46 | -------------------------------------------------------------------------------- /OFEP/flagd-grpc-sync.md: -------------------------------------------------------------------------------- 1 | --- 2 | date: 2023-02-03 3 | title: gRPC sync support to Flagd 4 | status: Approved 5 | authors: [Kavindu Dodanduwa] 6 | tags: [flagd] 7 | 8 | --- 9 | # Add gRPC sync support to flagd 10 | 11 | ## State: APPROVED 12 | 13 | This OFEP proposes to introduce gRPC syncs to flagd. gRPC sync will act similar to existing remote HTTP URL syncs. But 14 | going beyond periodic pulls, flagd can utilize gRPC server streaming to receive near real-time updates, pushed from a 15 | flag management system. 16 | 17 | ## Background 18 | 19 | gRPC server streaming allows clients to listen and react to server-pushed data. For flagd, this perfectly matches the 20 | ISync interface and current sync mechanism implementations. 21 | 22 | The gRPC schema will be defined by flagd and supporting flag management system(s) will then implement the contract. 23 | 24 | ![bidirectional communication](images/grpc-sync/bidirectional-communication.png "bidirectional communication") 25 | 26 | 27 | Further, grpc server push can be expanded to have `event types` such as flag additions, updates and deletions, giving more 28 | performant connectivity between flagd and flag management system. Performance improvements come from reduced payload 29 | size(single flag change vs all flags) and not having connection establishment overhead thanks to streaming. 30 | 31 | Note that the implementation complexity of `event types` lives at grpc server implementation. The implementation may use 32 | a state management system to derive the matching event type for a specific flag configuration change. In any case, 33 | flagd must not maintain any state (i.e- flagd must be stateless) and only react on the sync type to update flag 34 | configurations. 35 | 36 | ![dataflow](images/grpc-sync/dataflow.png "dataflow") 37 | 38 | ### Tasks 39 | 40 | Following are the main tasks I am proposing for the implementation. 41 | 42 | - [x] POC implementation - https://github.com/open-feature/flagd/pull/297 43 | - [ ] OFEP approval 44 | - [ ] Introduce basic grpc sync, with minimal configuration options 45 | - [ ] Introduce additional options, such as TLS certificates, token authentication on top of existing solution 46 | 47 | #### SSL certificates and token authentication 48 | 49 | Consider the GRPC authentication example provided through official Go guide - [Link](https://github.com/grpc/grpc-go/tree/master/examples/features/authentication) 50 | 51 | With a similar approach, it is possible to establish TLS connections and enable token based authentication/authorization 52 | between flagd and flag management system. -------------------------------------------------------------------------------- /OFEP/flagd-project-charter.md: -------------------------------------------------------------------------------- 1 | --- 2 | date: 2023-07-11 3 | title: FlagD as full sub-project 4 | status: Approved 5 | authors: [Alois Reitbauer] 6 | tags: [sdk, spec, ofo, flagd] 7 | 8 | --- 9 | # Make FlagD a full sub-project of OpenFeature 10 | 11 | ## State: APPROVED 12 | 13 | 14 | This OFEP proposes to make flagD a full subproject of OpenFeature. The project will get a dedicated website, documentation, roadmap and charter. 15 | 16 | ## Background 17 | 18 | flagD started as small reference implementation for a developer-focussed feature evaluation mechanism, enabling OpenFeature SDK users to experiment with an end-to-end solution. Over time the project has grown in functionality and become stable and is now also used in production settings. 19 | 20 | In the past there was also some misunderstanding of the relationship of OpenFeature and flagD. 21 | 22 | Therefore, the project should become a full sub-project of open feature. 23 | 24 | ## Proposal 25 | 26 | flagD will become a dedicated sub project of the OpenFeature project. Documentation for flagD and all examples for OpenFeature will be moved to a dedicted space under the newly created domain ``flagd.dev``. 27 | 28 | The sub project gets it's own dedicated charter to ensure the project has clearly defined goals and scope. As a sub project flagD will have its own maintainers but will still be under the governance of the OpenFeature project. 29 | 30 | Below is the charter for flagD as defined by the OpenFeature governance board. 31 | 32 | 33 | ### Charter 34 | 35 | FlagD is an open-source project which provides a portable, lightweight, production-ready, and OpenFeature-compliant feature flag evaluation daemon, along with a supporting k8s operator and set of OpenFeature SDK providers. 36 | 37 | The flagD project aims to provide a developer-focused, cloud-native, lightweight and extensible feature evaluation engine. 38 | 39 | * **Developer-focused** means that flagD is configured in a declarative approach using “flags-as-code” 40 | * **Cloud-native** means it focuses on seamless, standardised integration with cloud-native tools and practices like GitOps. * These components - like the operator - make the integration easy but are not required. 41 | * **Lightweight** means that flagD provides a compact and executable binary with a design focus on simplicity and ultra-fast, low-latency execution. flagD is implemented as a stateless, highly-scalable service which only requires a local flag definition or a configured endpoint to acquire the definition. 42 | * **Extensible** means that other projects and feature management tools can integrate easily using well-defined interfaces for rule evaluation requests and rule set definition. 43 | * **OpenFeature compliance** means flagd serves as a production-ready reference implementation of the OpenFeature specification and demonstrates the value of an open standard for feature flagging. 44 | 45 | The following topics are out of scope for the flagD projects: 46 | * Identity, storage, and attestation mechanisms 47 | * Distributed architecture ( High availability quorum, voting, leader/follower, failover mechanics) 48 | * Vendor modules dependencies 49 | * A feature management and/or analytics solution (but could provide the foundations for one). The project will highlight open-source and commercial implementations which provide these capabilities and are OpenFeature compliant. 50 | -------------------------------------------------------------------------------- /OFEP/flagd-sockets.md: -------------------------------------------------------------------------------- 1 | --- 2 | date: 2022-08-09 3 | title: Flagd Sockets 4 | status: Approved 5 | authors: [Alex Jones] 6 | tags: [flagd] 7 | 8 | --- 9 | 10 | # Flagd sockets 11 | 12 | ## State: APPROVED 13 | 14 | Often when flagD is required to talk to another application within the same pod, it is far faster to use a unix socket 15 | than the full TCP/IP stack. There are also permission benefits around using a socket in terms of file ownership. 16 | This OFEP outlines an approach to use gRPC over Unix sockets to enable this. 17 | 18 | ![communication](images/flagd-sockets/communication.png "communication") 19 | 20 | 21 | ## Background 22 | 23 | It is possible within golang to use a unix socket as a `*net.Conn` 24 | The following illustration exemplifies how it can be used to create the underlying gRPC transport. 25 | 26 | ``` 27 | conn, err := net.DialUnix("unix", nil, "unix://proc/flagD.sock") 28 | return conn, err 29 | 30 | conn, err := grpc.Dial(server_file, grpc.WithInsecure(), grpc.WithDialer(UnixConnect)) 31 | if err != nil { 32 | log.Fatal("did not connect: %v", err) 33 | } 34 | defer conn.Close() 35 | ``` 36 | 37 | This shows how the machinery is already present to enable IPC through this interface. 38 | 39 | ## Proposal 40 | 41 | I propose that we introduce an additional layer of gRPC options in the `grpc_service.go` in flagD. 42 | This would allow a new `serveSocket` method to be created and facilitate the IPC functionality. 43 | _Note this wouldn't be a TLS enabled transport_ 44 | 45 | ### Implementation 46 | 47 | - [x] Create a socket path parameter within flagD 48 | - [ ] `grpc_service.go` to support unix sockets through a new method that returns a net connection 49 | - [ ] Modifiy the existing `http_service` code to leverage a similar net connection start 50 | - [ ] Write tests 51 | -------------------------------------------------------------------------------- /OFEP/images/cloud-native-pattern/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-feature/ofep/d4b3397bd0382c21584f5cc17bcfe2bcf0c99643/OFEP/images/cloud-native-pattern/architecture.png -------------------------------------------------------------------------------- /OFEP/images/cloud-native-pattern/config-reload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-feature/ofep/d4b3397bd0382c21584f5cc17bcfe2bcf0c99643/OFEP/images/cloud-native-pattern/config-reload.png -------------------------------------------------------------------------------- /OFEP/images/cloud-native-pattern/e2e-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-feature/ofep/d4b3397bd0382c21584f5cc17bcfe2bcf0c99643/OFEP/images/cloud-native-pattern/e2e-diagram.png -------------------------------------------------------------------------------- /OFEP/images/cloud-native-pattern/host-to-agent-com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-feature/ofep/d4b3397bd0382c21584f5cc17bcfe2bcf0c99643/OFEP/images/cloud-native-pattern/host-to-agent-com.png -------------------------------------------------------------------------------- /OFEP/images/cloud-native-pattern/remote-provider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-feature/ofep/d4b3397bd0382c21584f5cc17bcfe2bcf0c99643/OFEP/images/cloud-native-pattern/remote-provider.png -------------------------------------------------------------------------------- /OFEP/images/cloud-native-pattern/single-point-of-failure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-feature/ofep/d4b3397bd0382c21584f5cc17bcfe2bcf0c99643/OFEP/images/cloud-native-pattern/single-point-of-failure.png -------------------------------------------------------------------------------- /OFEP/images/cue-upstream/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-feature/ofep/d4b3397bd0382c21584f5cc17bcfe2bcf0c99643/OFEP/images/cue-upstream/architecture.png -------------------------------------------------------------------------------- /OFEP/images/flagd-sockets/communication.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-feature/ofep/d4b3397bd0382c21584f5cc17bcfe2bcf0c99643/OFEP/images/flagd-sockets/communication.png -------------------------------------------------------------------------------- /OFEP/images/grpc-sync/bidirectional-communication.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-feature/ofep/d4b3397bd0382c21584f5cc17bcfe2bcf0c99643/OFEP/images/grpc-sync/bidirectional-communication.png -------------------------------------------------------------------------------- /OFEP/images/grpc-sync/dataflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-feature/ofep/d4b3397bd0382c21584f5cc17bcfe2bcf0c99643/OFEP/images/grpc-sync/dataflow.png -------------------------------------------------------------------------------- /OFEP/images/kubecon-demo/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-feature/ofep/d4b3397bd0382c21584f5cc17bcfe2bcf0c99643/OFEP/images/kubecon-demo/architecture.png -------------------------------------------------------------------------------- /OFEP/images/kubernetes-sync-service/communication.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-feature/ofep/d4b3397bd0382c21584f5cc17bcfe2bcf0c99643/OFEP/images/kubernetes-sync-service/communication.png -------------------------------------------------------------------------------- /OFEP/images/kubernetes-sync-service/notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-feature/ofep/d4b3397bd0382c21584f5cc17bcfe2bcf0c99643/OFEP/images/kubernetes-sync-service/notification.png -------------------------------------------------------------------------------- /OFEP/images/ofo-client-support/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-feature/ofep/d4b3397bd0382c21584f5cc17bcfe2bcf0c99643/OFEP/images/ofo-client-support/architecture.png -------------------------------------------------------------------------------- /OFEP/inline-evaluation.md: -------------------------------------------------------------------------------- 1 | --- 2 | date: 2023-03-16 3 | title: Inline Evaluation of Flag Rules 4 | status: Rejected 5 | authors: [Justin Abrahms, Pete Hodgson] 6 | tags: [flagd] 7 | 8 | --- 9 | # Inline Evaluation of Flag Rules 10 | 11 | ## State: REJECTED 12 | 13 | This OFEP seeks to find a solution for near-zero latency flag evaluation for server-side contexts (e.g. not mobile or client-side web). 14 | 15 | ## Background 16 | 17 | Today, most flag providers make API calls to backing services to understand if a given flag should return e.g. `true` or `false`. Even if that API call is going to a kubernetes sidecar like the flagD case, it introduces latency which may not be acceptable in high scale use-cases. The flag's rules are often evaluated on another service because determining whether the provided context matches a given set of rules requires computation which may be non-trivial or may take into account data not known by the caller. 18 | 19 | ## Proposal 20 | 21 | To address this, I'd like to define a mechanism by which the rules can be returned to the openfeature client, and it can determine the answer of which value to return. The intent is to update these rules outside of the scope of the request lifecycle, such as at startup and/or on an interval basis. 22 | 23 | ## Updated Provider API 24 | 25 | We would offer a stock `RuleEvaluatingProvider` that would implement the `getBooleanEvaluation` et al methods given a rule state. That rule state would be fetched by a `RuleFetcher`, which provider authors would implement. That fetcher would, for instance, download the rules from the data store and set up an appropriate update interval in the background. 26 | 27 | When "provider ready" events are in use, `RuleEvaluatingProvider` will not emit ready until the `RuleFetcher` emits ready. 28 | 29 | ## Rule Format 30 | 31 | The rule format use [JsonLogic](https://jsonlogic.com/), which is already setup in `flagD` and has broad support for various languages. 32 | 33 | ### What it might look like 34 | 35 | ```java 36 | public class MyRulesProvider implements RuleFetcher { 37 | @Override 38 | public JsonLogic fetch() { 39 | // your code to get the flags and their rules and convert it to jsonlogic goes here 40 | } 41 | } 42 | 43 | // in your app setup code.. 44 | OpenFeature.getInstance().setProvider(new RuleEvaluatingProvider(new MyRulesProvider(API_KEY))) 45 | ``` 46 | -------------------------------------------------------------------------------- /OFEP/kubecon-demo.md: -------------------------------------------------------------------------------- 1 | --- 2 | date: 2022-07-12 3 | title: Kubecon Demo 4 | status: Approved 5 | authors: [Alex Jones] 6 | tags: [flagd, ofo] 7 | 8 | --- 9 | # Kubecon demo 10 | 11 | ## State: APPROVED 12 | 13 | I believe we need to have some really good examples of feature-flagging on both the client/developer side and also used in infrastructure scenarios. 14 | 15 | I propose that we combine several key technologies to build a very compelling demo. 16 | 17 | Open feature operator + flagD 18 | Sigstore/cosign 19 | CUE 20 | The following proposal is to build a demo image admission application that could potentially be spun out into a real project eventually. The tie into OpenFeature is that it would read its flag configuration to decide whether the image signing validator is turned on or off. 21 | 22 | Comments welcome. 23 | 24 | ![architecture](images/kubecon-demo/architecture.png "architecture") 25 | 26 | ## Work involved 27 | 28 | - Write a CUE based ievaluator 29 | - Extend Openfeature operator to read CUE based configuration 30 | - Write the demo image admission controller 31 | 32 | 33 | ## End-user takeaways 34 | 35 | Featureflags can be used at the infrastructure level 36 | OpenFeature operator and FlagD allow you to build against them with no real work required to integrate ( evaluator included ). -------------------------------------------------------------------------------- /OFEP/kubernetes-sync-service.md: -------------------------------------------------------------------------------- 1 | --- 2 | date: 2022-09-15 3 | title: Inotfiy Interface 4 | status: Approved 5 | authors: [Alex Jones] 6 | tags: [flagd, ofo] 7 | 8 | --- 9 | # Inotfiy Interface 10 | 11 | ## State: APPROVED 12 | 13 | FlagD is often used in the context of Kubernetes. 14 | 15 | It is typically deployed by OFO as a companion sidecar and uses a volume injected via configmap as the source of its flags to evaluate. 16 | 17 | I propose there may be a more optimal way to enable faster refreshing on flag changes by subscribing to a notifier against the Kubernetes API. This is inspired by this discussion with @therealmitchconnors and also the regression in being able to trigger configmap volume reloads through updating annotations within a deployment. 18 | 19 | This is not a silverbullet and I think for academic purposes alone it is worth exploring if we would want to move in this direction. 20 | 21 | ~~The only real benefit here is speed of flag updates.~~ 22 | 23 | _Benefits to be discussed within this document_ 24 | 25 | ## Design 26 | 27 | FlagD would start with the typical arguments of flagd start --sync-service kubernetes. 28 | This would use the token mounted from /var/run/secrets/kubernetes.io/serviceaccount to request to watch events for the FeatureFlagConfiguration. The FeatureFlagConfiguration could be determined by the pod getting its own annotations on start through the API and performing a look-up. 29 | 30 | 31 | The current sync mechanics should be compatible with the 32 | ``` 33 | type ISync interface { 34 | Fetch(ctx context.Context) (string, error) 35 | Notify(ctx context.Context, c chan<- INotify) 36 | } 37 | ``` 38 | 39 | 40 | 41 | ![communication](images/kubernetes-sync-service/communication.png "communication") 42 | 43 | 44 | ### Realtime updates 45 | - Shared informer factory will need to be extended to support the FeatureFlagConfiguration type through the restful API. 46 | ``` 47 | queue := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()) 48 | labelset := labels.Set(labelmap) 49 | optionsModifier := func(options *meta_v1.ListOptions) { 50 | options.LabelSelector = labels.SelectorFromSet(labelset).String() 51 | } 52 | 53 | informer := config.InformerFactory.InformerFor(&batch_v1.Job{}, func(client kubernetes.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { 54 | return cache.NewSharedIndexInformer( 55 | cache.NewFilteredListWatchFromClient(client.BatchV1().RESTClient(), "featureflagconfigurations", namespace, optionsModifier), 56 | &batch_v1.Job{}, 57 | resyncPeriod, 58 | cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, 59 | ) 60 | }) 61 | ``` 62 | 63 | I believe a _sharedinformer_ might be appropriate over indexed, unless we're looping over all the featureflagconfigurations in the cluster and they list in their hundreds. 64 | 65 | ![notification](images/kubernetes-sync-service/notification.png "notification") 66 | 67 | 68 | 69 | ## Benefits 70 | 71 | - Near real-time updates to flags as described within a Custom resource 72 | - Lays foundation for watching of specific flagD feature configuration types _as per previous issue discussions_ 73 | 74 | ## Caveats 75 | - Creates a deep reference to Open feature operator 76 | - Requires the design of failures modes for deletion of custom resources. 77 | - Additional load on the API Server 78 | - tightly couple FlagD to the Kubernetes golang bindings and limit multi-arch compatibility 79 | -------------------------------------------------------------------------------- /OFEP/metric-hooks.md: -------------------------------------------------------------------------------- 1 | --- 2 | date: 2023-06-01 3 | title: Hooks for Metrics 4 | status: Approved 5 | authors: [Michael Beemer, Todd Baert, Kavindu Dodanduwa, Justin Abrahms, Giovanni Liva] 6 | tags: [spec, sdk] 7 | 8 | --- 9 | # Hook for metrics 10 | 11 | ## State: APPROVED 12 | 13 | This OFEP proposes to introduce an OpenFeature hook for OpenTelemetry metrics. 14 | 15 | ## Background 16 | 17 | We already have OpenTelemetry Span support through hooks. Similarly, we can provide a dedicated hook for OpenTelemetry 18 | metrics. Providing an easy method to collect standardized telemetry data will make OpenFeature attractive for both users and vendors. 19 | 20 | ## Proposal 21 | 22 | The proposal here is to define a set of metrics that can be used with different hook stages. For example, error metrics 23 | can be collected at the hook's `error` stage. Going with this background, I propose the following metrics. 24 | 25 | ## Metrics 26 | 27 | All metrics defined through this proposal carry OpenTelemetry semantic conventions defined attributes(dimensions)[1]. 28 | 29 | - feature_flag.key: The unique identifier of the feature flag 30 | - feature_flag.provider_name: The name of the service provider that performs the flag evaluation 31 | - feature_flag.variant: SHOULD be a semantic identifier for a value. If one is unavailable, a stringified version of 32 | the value can be used 33 | 34 | Given below is the list of metrics proposed and their usage, 35 | 36 | ### feature_flag.evaluation_request_total 37 | 38 | A counter[2] based metric to calculate the number of flag evaluation requests. This is recorded at `before` stage of 39 | the hook. 40 | 41 | This metric is useful to understand flag evaluation requests received through OpenFeature SDK. 42 | 43 | ### feature_flag.evaluation_success_total 44 | 45 | A counter[2] based metric to calculate the number of successful flag evaluations. This is recorded at `after` stage of 46 | the hook. 47 | 48 | This metric is useful for understanding successful flag evaluations. This metric contains the following extra dimension(s), 49 | 50 | - reason : evaluation reason extracted from flag resolution details[3] 51 | 52 | ### feature_flag.evaluation_error_total 53 | 54 | A counter[2] based metric to calculate the number of failed flag evaluations. This is recorded at the `error` stage of 55 | the hook. 56 | 57 | This metric is useful for understanding flag evaluation errors. This metric contains the following extra dimension(s), 58 | 59 | - exception : error/exception message extracted from the evaluation error 60 | 61 | ### feature_flag.evaluation_active_count 62 | 63 | An UpDownCounter[4] based metric to calculate the number of active flag evaluations currently going through 64 | OpenFeature SDK. This is increased at `before` stage and decreased at `finally` stage. 65 | 66 | Given the evaluation is fast, the value observed here can be 0. However, when there are bottlenecks or provider 67 | slowdowns, this will be a non-zero value. 68 | 69 | ## Extra dimensions 70 | 71 | If needed, extra dimensions can be added to any of the above metrics. To provide this flexibility, language specific 72 | implementations should support constructor options to the hook. For example, in Go, this can look like below, 73 | 74 | ```go 75 | NewMetricsHook(reader, 76 | WithFlagMetadataDimensions( 77 | DimensionDescription{ 78 | Key: "scope", 79 | Type: String, 80 | })) 81 | ``` 82 | 83 | Given below are proposed extra dimensions. 84 | 85 | ### Scope (mapped from flag metadata) 86 | 87 | credits - Michael Beemer 88 | 89 | It is possible to add an additional dimension representing the configuration of the feature flag being evaluated. A 90 | feature flag usually has a scope such as a project, workspace, namespace, or application. This can be further 91 | expanded to environment-specific configurations such as dev, hardening, and production or the cloud provider such as 92 | AWS, Azure or GCP. Adding this dimension through an agreed attribute name (suggested name - `scope`) benefits metric 93 | evaluations (ex:- drill down to cloud provider specific flag evaluations). 94 | 95 | 96 | ## Expansion options 97 | 98 | Below are future expansions that can be built on top of the metrics hook. These options will not be 99 | implemented as they require further discussions and agreements from the community. 100 | 101 | ### Metric to measure latency 102 | 103 | credits - Justin Abrahms 104 | 105 | Flag evaluation latency can be calculated with time measurements between `finally` and `before` stages. However, 106 | this requires time measurement to be shared between two stages, which require either a context propagation or a shared 107 | variable (potentially a map). Alternatively, SDK could mark the evaluation start timestamp to enhance the accuracy 108 | of the measurement 109 | 110 | ## Implementation considerations 111 | 112 | Most of the OpenFeature SDKs already have support for Span hooks. The metrics hook proposed through this OFEP should be 113 | implemented into the same package to reduce release and maintenance efforts. However, if there is a significant 114 | impact (ex:- dependency size for example in java jar), then the implementation may be done in a dedicated package. 115 | 116 | ## Code Example 117 | 118 | Consider following coding example in Go for usage of the hook. 119 | 120 | ```go 121 | 122 | // Reader should be derived or injected 123 | var reader metric.Reader 124 | 125 | // Derive metric hook from reader 126 | metricsHook := hooks.NewMetricsHook(reader) 127 | 128 | // Register OpenFeature API hooks 129 | openfeature.AddHooks(metricsHook) 130 | ``` 131 | 132 | Injected `metric.Reader` will perform the metric export and API is simple enough for developers. 133 | 134 | ### References 135 | 136 | [1] - https://opentelemetry.io/docs/specs/otel/logs/semantic_conventions/feature-flags/ 137 | 138 | [2] - https://opentelemetry.io/docs/specs/otel/metrics/api/#counter 139 | 140 | [3] - https://openfeature.dev/specification/types#resolution-details 141 | 142 | [4] - https://opentelemetry.io/docs/specs/otel/metrics/api/#updowncounter -------------------------------------------------------------------------------- /OFEP/ofo-flag-service.md: -------------------------------------------------------------------------------- 1 | --- 2 | date: 2023-03-24 3 | title: Flag Service Deployment by OFO 4 | status: Approved 5 | authors: [Skye Gill] 6 | tags: [flagd, ofo] 7 | 8 | --- 9 | # Flag service deployment driven by OpenFeature Operator 10 | 11 | ## State: APPROVED 12 | 13 | Currently, OpenFeature Operator (OFO) manages the deployment of a flag provider (e.g. flagd) by appending it to a pod's containers (sidecar pattern), thereby allowing containers within the pod to route to it. This OFEP campaigns for the extension of OFO to manage flag providers by abstracting a Kubernetes Deployment resource. 14 | 15 | ## Background 16 | 17 | The driving force behind this is to simplify the deployment of flag providers for use by client side applications. An [OFEP was drafted](./OFEP-ofo-flagd-client-support.md) to achieve this but subsequently withdrawn (reasons noted within). This OFEP recognises the limitations of the previous, presenting a modular solution with a broader scope. 18 | 19 | ## Proposal 20 | 21 | Introduce a FlagService custom resource definition (CRD) and controller. 22 | The controller uses the configuration defined within the custom resource (CR) to create a Service (in OFO's namespace) and a Deployment of a flag provider (backed by the Service) in the same namespace as the CR. This is a common deployment pattern permitting access by any component that routes to the created Service (e.g. Ingress/Load Balancer). OFO already manages the [sidecar deployment pattern](https://learn.microsoft.com/en-us/azure/architecture/patterns/sidecar) to achieve the goal of an internally routable flag provider. In contrast, the described FlagService pattern permits an externally routable flag provider. 23 | 24 | ### RBAC 25 | 26 | OFO already has RBAC to Deployments but not Services so (at minimum) the following is required. 27 | 28 | ``` 29 | apiVersion: rbac.authorization.k8s.io/v1 30 | kind: Role 31 | metadata: 32 | creationTimestamp: null 33 | name: manager-role 34 | namespace: open-feature-operator-system 35 | rules: 36 | - apiGroups: 37 | - "" 38 | resources: 39 | - services 40 | verbs: 41 | - create 42 | - delete 43 | - get 44 | - list 45 | - patch 46 | - update 47 | - watch 48 | ``` 49 | 50 | This restricts OFO's Service mutation scope to within its namespace. 51 | -------------------------------------------------------------------------------- /OFEP/ofo-flagd-client-support.md: -------------------------------------------------------------------------------- 1 | --- 2 | date: 2023-03-24 3 | title: Flagd Client Support driven by OFO 4 | status: Withdrawn 5 | authors: [Skye Gill] 6 | tags: [flagd, ofo] 7 | 8 | --- 9 | # Flagd client support driven by OpenFeature Operator 10 | 11 | ## State: WITHDRAWN 12 | 13 | Client side applications (e.g. web/mobile) could benefit from access to feature flags. This OFEP outlines the feasibility and benefits of extending [OpenFeature Operator (OFO)](https://github.com/open-feature/open-feature-operator) to manage the deployment of flagd with access from external traffic (e.g. client side applications). 14 | 15 | _Withdrawn for the following reasons:_ 16 | - _There's a huge variety of unknown deployment patterns by potential users. Catering to one likely means scope creep to support many in future. Perhaps this is the future path of OFO but an agreement has been reached that it is best to keep things simple and minimally scoped at this stage (until common deployment patterns are more established)._ 17 | - _OFO would require CRUD permissions for HTTPRoutes & Gateways, introducing security concerns._ 18 | - _Kubernetes Gateway API is still in beta._ 19 | 20 | _[OFEP for an alternative solution](./OFEP-ofo-flag-service.md)._ 21 | 22 | ## Background 23 | 24 | OFO already manages the deployment of flagd in server side contexts by injecting it as a sidecar container to an existing workload. This permits the workload to communicate with flagd due to the inherent nature of networking between containers within a pod. Conversely, the routing of client side applications to flagd is not trivial. OFO could bear this burden by configuring the cluster as necessary (see the proposal below) to facilitate the deployment of externally accessible (by client side applications) flagd. 25 | 26 | ## Assumptions 27 | The (simplified) deployment pattern is as follows 28 | 29 | - Deployment of a client side application exposed by a Service 30 | - [Gateway](https://gateway-api.sigs.k8s.io/api-types/gateway/) exposing the app's Service via a [HTTPRoute](https://gateway-api.sigs.k8s.io/api-types/httproute/) and listener 31 | 32 | The networking infrastructure for ingesting external traffic is already in place for the existing client side application. 33 | 34 | ## Proposal 35 | 36 | Introduce a custom resource definition (CRD) with configuration permitting OFO to create a deployment of flagd and inject the necessary networking configuration to route incoming traffic from the existing Gateway to flagd (via a new HTTPRoute/GRPCRoute). 37 | This results in flagd being externally accessible. 38 | 39 | The following diagram depicts the architecture. Resources inside the green box are created via the proposed CRD. 40 | 41 | ![OFO client architecture](images/ofo-client-support/architecture.png "OFO Client Architecture") 42 | 43 | Note: The example depicted by the diagram uses a subdomain to route the request to flagd but [path matching is also possible](https://gateway-api.sigs.k8s.io/api-types/httproute/#matches). 44 | 45 | ## Limitations 46 | [Kubernetes Gateway API (KGA)](https://gateway-api.sigs.k8s.io/) is in beta (as are its implementors). This OFEP could be extended (or a new one created) to also support [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/). 47 | The [GRPCRoute](https://gateway-api.sigs.k8s.io/api-types/grpcroute/) is experimental. 48 | 49 | ## Proof of concept 50 | 51 | A proof of concept has been created [here](https://github.com/open-feature/open-feature-operator/issues/371#issuecomment-1468511819) with an executable demonstration of the proposal. 52 | -------------------------------------------------------------------------------- /OFEP/provider-client-mapping.md: -------------------------------------------------------------------------------- 1 | --- 2 | date: 2023-04-27 3 | title: Provider-Client Mapping 4 | status: Approved 5 | authors: [Justin Abrahms] 6 | tags: [spec] 7 | 8 | --- 9 | # Provider to client mapping 10 | 11 | ## State: APPROVED 12 | 13 | The goal of this OFEP is to describe a way that application authors can selectively determine which provider a given client uses. This means that they should be able to set a provider for a library that is different than what they use in their main app. 14 | 15 | ## Background 16 | 17 | Today, we operate with [a single provider stored on a global singleton](https://github.com/open-feature/spec/blob/74c373e089ad77bf8cac84f3d93c00c945ff3a8a/specification/sections/01-flag-evaluation.md?plain=1#L25). Especially in large apps, which may have different provider needs, this is limiting. Teams could solve this in user-space with a sort of `MultiplexingProvider` which takes in N other providers. Sadly, this introduces latency (calling provider 1 before falling through to provider 2 isn't free) and waste. Additionally, when it comes to testing, the "there can be only one!" nature of global singletons makes things a giant pain. 18 | 19 | ## Proposal 20 | 21 | To address that, we will allow application authors to provide mappings of client names to a provider of their choosing. This should allow a great degree of flexibility for app authors to choose the flag-config store that works for that particular use-case. 22 | 23 | ### Example Implementation 24 | 25 | Authors may optionally use [a named client](https://github.com/open-feature/spec/blob/74c373e089ad77bf8cac84f3d93c00c945ff3a8a/specification/sections/01-flag-evaluation.md?plain=1#L60), which we already support. If they don't pick a name, it falls through to the configured "default" provider (or no provider if none are set). 26 | 27 | ```java 28 | # example library code 29 | var client = OpenFeature.getInstance().getClient('my-sweet-library') 30 | if client.getBooleanValue('redesign-enabled?', false) { 31 | # ... 32 | } 33 | ``` 34 | 35 | ``` 36 | # application author 37 | OpenFeature.setProvider(OpenFeature.DEFAULT_PROVIDER, new FlagDProvider()) 38 | OpenFeature.setProvider('my-sweet-library', new FileBasedProvider('path/to/myfile')) 39 | ``` 40 | 41 | At this point, things will use flagD by default. The example library will use a file-based provider. 42 | -------------------------------------------------------------------------------- /OFEP/provider-hook.md: -------------------------------------------------------------------------------- 1 | --- 2 | date: 2022-07-30 3 | title: Provider Hooks 4 | status: Approved 5 | authors: [Shantanu Sen] 6 | tags: [sdk, spec] 7 | 8 | --- 9 | # Provider Hooks 10 | 11 | ## State: APPROVED 12 | 13 | As a part of the Provider implementation by the Flag Management Systen, there is a need to have standardized support for hooks specific to the Provider. The 14 | Provider Hook(s) should be specific to the provider implementation and implement the various defined stages of the Hook interface as applicable to the 15 | specific provider. 16 | 17 | ## Assumptions 18 | 19 | The Provider Hook is transparent to the application developer. The Provider may choose to document certain default behavior/feature in specific stages of the 20 | Provider Hook as relevant to the application and the application developer may be allowed to override the default feature. 21 | 22 | ## Example Use Case 23 | 24 | Flag Management System X provides the option to add Experimentation as an extension of Feature Flag. The user creates a Feature Flag, runs basic on/off 25 | evaluations of the Feature targeting specific audience and then wants to run A/B tests and hence adds the Experimentation aspects to the Feature Flag. 26 | 27 | The Provider needs to generate event specific to the Experimentation implementation on successful flag evaluation. The system wants to use a Provider Hook 28 | to do the following for each evaluation 29 | 1. Run initialization operations (Use the _before_ stage of the Hook) 30 | 2. Run an event publish operation on successful flag evaluation operation (Use the _after_ stage of the Hook) 31 | 3. Provide the option to the application developer to override the event publish operation 32 | 1. The application developer may choose not to immediately publish the event on evaluation, but decide to publish it (or not) later based on other 33 | application specific logic 34 | 35 | For applications not required to override the default behavior, there is no extra step needed to add any other hooks. The Provider Hook is transparent to 36 | the application developer. 37 | 38 | ## Example Workflow 39 | 40 | 1. At the time of registering the provider (OpenFeature.setProvider), Provider implementation can register one or more Provider Hook(s) 41 | 2. Provider Hook implements the _before_ and _after_ and any other relevant stages to run the relevant provider logic. 42 | 3. Provider documents the relevant default behavior (e.g. automatic publish of Experimentation event on successful evaluation) 43 | 4. Provider provides an implementation of an Invocation Hook to override this default behavior as a part of the Provider SDK. 44 | 5. If needed, the application developer uses the invocation hook to override specific default behavior of the provider by using Hook Hints e.g. turn off 45 | default event publish behavior 46 | 47 | ## Hook Ordering 48 | 49 | The Hooks should be executed in specific order. 50 | 51 | * before: API, Client, Invocation, Provider 52 | * Provider does initialization after Invocation 53 | * after: Provider, Invocation, Client, API 54 | * error (if applicable): Provider, Invocation, Client, API 55 | * finally: Provider, Invocation, Client, API 56 | 57 | -------------------------------------------------------------------------------- /OFEP/provider-metadata-capability-discovery.md: -------------------------------------------------------------------------------- 1 | --- 2 | date: 2023-04-15 3 | title: Capability discovery for providers 4 | status: Approved 5 | authors: [Todd Baert] 6 | tags: [specification, sdk] 7 | 8 | --- 9 | # Capability discovery for providers 10 | 11 | ## State: APPROVED 12 | 13 | This OFEP proposes a solution for "capability discovery" in the context of providers, in order to signal to the SDK and `application authors` what functionality is available in a particular provider. 14 | 15 | ## Background 16 | 17 | As the OpenFeature specification evolves, it's to be expected that some components may not be able to support certain functionality. For instance, some providers may not support flag change events (as described in https://github.com/open-feature/ofep/pull/25). 18 | 19 | ## Proposal 20 | 21 | This OFEP proposes that the [`provider metadata`](https://openfeature.dev/docs/specification/sections/providers#requirement-211) be extended to include optional properties that denote which functions are available on the implementing provider. The SDK can then make intelligent decisions and log warnings if capabilities that are not supported by the provider in question, are used. For example, if an `application author` adds an event handler but the registered provider doesn't support events, the SDK can log a warning. 22 | 23 | ### Example Implementation 24 | 25 | ```typescript 26 | 27 | import { Provider, Capabilities } from '@openfeature/js-sdk'; 28 | 29 | /** 30 | * The following provider supports `SomeFeature`, `ProviderEvents`, but not `SomeOtherFeature`, which are defined by the SDK. 31 | */ 32 | class SomeFeatureProvider implements Provider { 33 | readonly metadata = { 34 | name: 'Some Feature Provider', 35 | capabilities: { 36 | [Capabilities.SomeFeature]: true, 37 | [Capabilities.ProviderEvents]: true, 38 | [Capabilities.SomeOtherFeature]: false, 39 | } 40 | }; 41 | 42 | // ...implementation... 43 | } 44 | ``` 45 | -------------------------------------------------------------------------------- /OFEP/sdk-e2e-test-strategy.md: -------------------------------------------------------------------------------- 1 | --- 2 | date: 2023-07-25 3 | title: SDK end-to-end test strategy 4 | status: Approved 5 | authors: [Michael Beemer, Todd Baert, Kavindu Dodanduwa, Justin Abrahms] 6 | tags: [specification, sdk, flagd] 7 | 8 | --- 9 | # SDK end-to-end test strategy 10 | 11 | ## State: APPROVED 12 | 13 | This OFEP proposes to introduce a simplified process to write SDK end-to-end(e2e) tests 14 | 15 | ## Background 16 | 17 | SDKs are at the core of OpenFeature as they contain language specific implementations of the OpenFeature specification [^1] 18 | 19 | For testing of the SDKs, current approach uses gherkin step definitions [^2] contained in OpenFeature/test-harness [^3] repository. 20 | And the test implementations use a real instance of flagd [^4] to validate test rules. 21 | 22 | The current approach however comes with a problem. As SDK tests rely on flagd, these tests rely on SDK contributions [^5]. 23 | This means: 24 | 25 | - SDK cannot introduce tests until flagd contribution is there 26 | - Breaking changes require disabling e2e tests until 27 | - Changes propagate to respective SDK contribution repository 28 | - flagd implements the breaking change 29 | - Cannot write tests till 30 | - flagd implements the feature 31 | 32 | Besides, there is a circular dependency among `Go SDK` - `Go SDK Contribution` - `flagd` [^6] which will become harder to maintain in the long run. 33 | 34 | ## Proposal 35 | 36 | Given OpenFeature has a lot of community interest and SDKs, I am proposing to introduce a simplified e2e test strategy. 37 | The test strategy must comply with the following: 38 | 39 | - Tests must not use mocks and must run against an OpenFeature compliant provider 40 | - Tests must be self-contained except for test definitions which are common to all SDKs 41 | - Tests must verify error scenarios 42 | 43 | To fulfill these needs, I am proposing a simple in-memory provider to be built into the SDK and maintained alongside the SDK implementation. 44 | 45 | ## In-memory provider 46 | 47 | In-memory provider should be bundled with SDK and must not have any unique external dependencies, other than the ones coming from SDK itself. 48 | While it mainly serves SDK testing, SDK consumers may use it for their use cases. Hence, the packaging, naming and access modifiers must be set appropriately. 49 | 50 | With this background, I am proposing following features for the in-memory provider, 51 | 52 | - Flag structure definition will be based on test requirements and must be minimal 53 | - Provider is initiated with a pre-defined set of flags provided to constructor 54 | - EvaluationContext support should be provided through callbacks/lambda expressions to minimize implementation 55 | - Must provide SDK contract implementations where needed (ex:- consider `NoOpProvider` [^7]) 56 | - Must continue to support new spec enhancements. For example, support for events 57 | 58 | [^1]: https://openfeature.dev/specification/ 59 | [^2]: https://cucumber.io/docs/gherkin/reference/#steps 60 | [^3]: https://github.com/open-feature/test-harness/tree/main 61 | [^4]: https://github.com/open-feature/flagd/tree/main 62 | [^5]: https://github.com/open-feature/java-sdk/blob/main/src/test/java/dev/openfeature/sdk/e2e/StepDefinitions.java#L3 63 | [^6]: https://github.com/open-feature/flagd/blob/main/flagd/go.mod#L69 64 | [^7]: https://github.com/open-feature/java-sdk/blob/main/src/main/java/dev/openfeature/sdk/NoOpProvider.java#L8 65 | -------------------------------------------------------------------------------- /OFEP/sdk-wait-provider-ready.md: -------------------------------------------------------------------------------- 1 | --- 2 | date: 2023-08-11 3 | title: SDK Wait Provider Ready 4 | status: Approved 5 | authors: [Michael Beemer, Kavindu Dodanduwa, Thomas Poignant] 6 | tags: [sdk] 7 | 8 | --- 9 | # SDK wait provider ready 10 | 11 | ## State: APPROVED 12 | Implement a mechanism to wait for the provider to be in a ready state. 13 | 14 | ## Background 15 | 16 | Provider can now define a `initialize` function called by the SDK when Openfeature users call the function `setProvider(...)`. 17 | The `initialize` function is called asynchronously and it is possible to get the information that the provider is ready while awaiting an initial ready event. 18 | 19 | In some languages such as `javascript` waiting for the event could be one line of code, but on other languages it could need way more boiler plate code to do that. 20 | ```javascript 21 | Openfeature.setProvider(my-provider) 22 | const client = Openfeature.getClient() 23 | await new Promise((resolve) => client.on(‘ready’, resolve)) 24 | ``` 25 | 26 | ```java 27 | OpenFeatureAPI.getInstance().setProvider("test", g); 28 | Client cli = OpenFeatureAPI.getInstance().getClient("test"); 29 | CompletableFuture completableFuture = new CompletableFuture<>(); 30 | OpenFeatureAPI.getInstance().onProviderReady(new Consumer() { 31 | @Override 32 | public void accept(EventDetails eventDetails) { 33 | completableFuture.complete(eventDetails); 34 | } 35 | }); 36 | completableFuture.get(); 37 | ``` 38 | 39 | ## Proposal 40 | It would be great to have a way in the SDKs to wait for the provider to be ready. 41 | 42 | We expose a dedicated blocking `setProvider` along with current async stepProvider. 43 | 44 | ### Examples 45 | ```javascript 46 | // Non blocking setProvider 47 | Openfeature.setProvider(myprovider) 48 | 49 | // Blocking setProvider 50 | await Openfeature.setProviderAndWait(myprovider) 51 | const client = Openfeature.getClient() 52 | ``` 53 | 54 | ```java 55 | // Non blocking setProvider 56 | OpenFeatureAPI.getInstance().setProvider(myprovider); 57 | Client cli = OpenFeatureAPI.getInstance().getClient(); 58 | 59 | // Blocking setProvider 60 | OpenFeatureAPI.getInstance().setProviderAndWait(myprovider); 61 | Client cli = OpenFeatureAPI.getInstance().getClient(); 62 | ``` 63 | 64 | 65 | ## Alternatives 66 | 67 | ### Alternative 1: make `setProvider` synchronous 68 | Probably not the most ideal solution but we could change the behavior of `setProvider` to wait for the initialize function to throw or return. 69 | *It is a breaking change from the actual implementation.* 70 | 71 | ### Alternative 2: Add a waiting function in the SDKs to wait for the provider to be ready 72 | Adding a waiting function will allow to block the SDK until the provider is ready. 73 | This new function will wait for a `ready` event or a timelimit for the provider to be ready. 74 | 75 | ```javascript 76 | Openfeature.setProvider(myprovider) 77 | const client = Openfeature.getClient() 78 | await client.isReady() 79 | ``` 80 | 81 | ### Alternative 3: Chain `waitReady()` with `setProvider(...)` 82 | Add a chain function to `setProvider(...)` to wait until the intialization is done. 83 | It will wait for the `initialize` function to throw or return. 84 | 85 | ```javascript 86 | await Openfeature.setProvider(myprovider).waitReady() 87 | const client = Openfeature.getClient() 88 | ``` 89 | 90 | ### Alternative 4: Add a blocking `setProvider` 91 | Expose a dedicated blocking `setProvider` along with current async stepProvider. 92 | 93 | > GRPC is using this kind of convention: 94 | > 95 | > - Java gRPC expose similar methods - `RouteGuideGrpc.newBlockingStub` vs `RouteGuideGrpc.newStub` [^1] 96 | > - In Go, gRPC uses method options that allow blocking calls and timeout based on context [^2]. 97 | 98 | [^1]: https://grpc.io/docs/languages/java/basics/#instantiating-a-stub 99 | [^2]: https://pkg.go.dev/google.golang.org/grpc#DialContext 100 | 101 | ```javascript 102 | // Non blocking setProvider 103 | Openfeature.setProvider(myprovider) 104 | 105 | // Blocking setProvider 106 | await Openfeature.setProviderAndWait(provider) 107 | const client = Openfeature.getClient() 108 | ``` 109 | Alternative names for this function can be `setProviderSync`, `setProviderBlocking`, `setProviderAndWait`. 110 | 111 | 112 | -------------------------------------------------------------------------------- /OFEP/single-context-paradigm.md: -------------------------------------------------------------------------------- 1 | --- 2 | date: 2023-05-19 3 | title: Single Context Paradigm 4 | status: Approved 5 | authors: [Todd Baert] 6 | tags: [sdk] 7 | 8 | --- 9 | # Single-context Paradigm 10 | 11 | ## State: APPROVED 12 | 13 | This draft outlines a "single-context" paradigm - an alternative pattern and set of APIs supporting client use-cases. 14 | 15 | ## Background 16 | 17 | In contrast with server-side or other service-type applications, client side apps typically operate in the context of a single user. 18 | Most feature flagging SDKs for these applications have been designed with this in mind. 19 | In summary, most client/web SDKs operate something like this: 20 | 21 | - an initialization occurs, which fetches evaluated flags in bulk for a given context (user) 22 | - the evaluated flags are cached in the SDK 23 | - flag evaluations take place against this cache, without a need to provide context (context was already used to evaluate flags in bulk) 24 | - Functions/methods are exposed on the SDK that signal the cache is no longer valid, and must be reconciled based on a context change. This frequently involves a network request or I/O operation. 25 | 26 | This paradigm doesn't fit well with the existing SDK, which, like most server-side SDKs, emphasizes realtime evaluation and context-per-evaluation. 27 | 28 | Though not all client side SDKs function in this way, those that do allow context per evaluation can conform to this model fairly easily. 29 | 30 | ## Design 31 | 32 | To better support the "single-context" paradigm we need to define some additional handlers and add some flexibility to current APIs. 33 | In short, the application author now sets context globally using the existing global context mutator on the OpenFeature API object. 34 | This single context is used by providers for initialization and fetching evaluated flag values in bulk. 35 | All evaluations functions are "context-less". 36 | When the context is changed, providers are signalled to update their cache of evaluated flags. 37 | 38 | ### Provider changes 39 | 40 | #### On-context-set handler 41 | 42 | Providers may need a mechanism to understand when their cache of evaluated flags must be invalidated or updated. An `on-context-set` handler can be defined which performs whatever operations are needed to reconcile the evaluated flags with the new context. The OpenFeature SDK calls this handler when the global context is modified. 43 | 44 | ```typescript 45 | 46 | interface Provider { 47 | //... 48 | 49 | // a handler called by the SDK when context is modified 50 | onContextSet?(oldContext: EvaluationContext, newContext: EvaluationContext): Promise 51 | 52 | //... 53 | } 54 | ``` 55 | 56 | While the `on-context-set` handler is executing, the cache of resolved flags may be considered "stale". `Provider authors` and `application authors` should understand the consequences of evaluating flags in this state. 57 | 58 | #### Initialize function 59 | 60 | Providers may need access to the static context when they start up. 61 | Passing this in the provider constructor is not always possible, and ergonomics are improved by separating configuration and evaluation. 62 | 63 | An `initialize` function can be optionally implemented by a provider, which defines an parameter for the static context. 64 | 65 | interface Provider { 66 | //... 67 | 68 | // a function called by the SDK when the provider becomes active 69 | initialize?(context: EvaluationContext): Promise 70 | 71 | //... 72 | } 73 | ``` 74 | 75 | > NOTE: The provider interface will retain the context parameter. 76 | The parameter will be supplied by the SDK from the global context. 77 | 78 | ### Client changes 79 | 80 | #### Remove evaluation-context parameter on evaluation functions 81 | 82 | Similarly to the changes in the provider, the evaluator functions on the client do not accept a context. 83 | 84 | ```typescript 85 | interface Client { 86 | //... 87 | 88 | // context parameter is removed from evaluation API 89 | getBooleanValue(flagKey: string, defaultValue: boolean, options?: FlagEvaluationOptions): boolean; 90 | getBooleanDetails(flagKey: string, defaultValue: boolean, options?: FlagEvaluationOptions): EvaluationDetails; 91 | 92 | //... 93 | } 94 | ``` 95 | 96 | #### Remove context mutator 97 | 98 | The global context has a one-to-one correspondence to the providers cache of evaluated flags. It's unreasonable or impossible to reconcile multiple client-level contexts with this state. The context mutator of the client must be removed. 99 | 100 | ### Hook changes 101 | 102 | #### Remove return value from before stage 103 | 104 | Context cannot be modified at evaluation, so `before` stage 105 | 106 | ```typescript 107 | 108 | export interface Hook { 109 | //... 110 | 111 | // before handler no longer optionally returns an EvaluationContext 112 | before?(hookContext: BeforeHookContext, hookHints?: HookHints): void; 113 | 114 | //... 115 | } 116 | 117 | ``` 118 | -------------------------------------------------------------------------------- /OFEP/transaction-context-propagation.md: -------------------------------------------------------------------------------- 1 | --- 2 | date: 2023-04-15 3 | title: Transaction Context Propagation 4 | status: Approved 5 | authors: [Michael Beemer, Todd Baert, Evan Bradley] 6 | tags: [specification, sdk] 7 | 8 | --- 9 | # Transaction Context Propagation 10 | 11 | ## State: APPROVED 12 | 13 | Flag evaluation may be affected by [evaluation context](https://openfeature.dev/docs/specification/sections/evaluation-context) data including global, client, invocation, and hook context. 14 | Currently, developers are responsible for explicitly defining and supplying evaluation context during flag evaluation. 15 | This proposal defines a new type of evaluation context called transaction context. 16 | 17 | _Transaction context_, is a container for transaction-specific evaluation context (e.g. user id, user agent, IP). 18 | With transaction context propagation, a developer can set transaction context where it's convenient (e.g. an auth service) and it will automatically be applied to all flag evaluations within a transaction (e.g. a request or thread). Transaction context will typically be propagated using a language-specific carrier such as thread local storage or another similar mechanism. 19 | 20 | This proposal covers context propagation for a single transaction within a process. 21 | Propagating evaluation context to child processes or services is out of scope of this OFEP. 22 | However, a proof of concept for how OpenTelemetry baggage could be leverage can be found [here][otel-baggage-poc]. 23 | 24 | ## Background 25 | 26 | Many languages provide a mechanism for storing data for the length of a single transaction. This can be used to store transaction context that can be automatically merged with the evaluation context before flag evaluation. The implementation will vary by technology. For example, Java may use [ThreadLocal][thread-local], Golang may use [Go Context][go-context], and Node may use [async hooks][async-hooks]. 27 | 28 | ## Proposal 29 | 30 | This proposal introduces a way to register a transaction context propagator to the global OpenFeature API. Once registered, transaction context can be augmented or mutated at any point in the transaction. When a flag is evaluated, the transaction context is merged with evaluation context based on the merge order defined below. 31 | 32 | ### Register Transaction Context Propagator 33 | 34 | In some runtimes (e.g. node), there isn't a native solution for transaction context propagation that would work in all situations. For that reason, it would be beneficial to provide the ability to register a transaction propagator on the global OpenFeature API. 35 | 36 | ```typescript 37 | /** 38 | * An example transaction context manager that utilizes async_hooks added in 39 | * node 12.17 and marked as stable in node 16.4 40 | */ 41 | class AsyncLocalStorageTransactionContext implements TransactionContextManager { 42 | private asyncLocalStorage = new AsyncLocalStorage(); 43 | 44 | getTransactionContext(): EvaluationContext { 45 | return this.asyncLocalStorage.getStore() ?? {}; 46 | } 47 | 48 | setTransactionContext( 49 | context: EvaluationContext, 50 | callback: () => void 51 | ): void { 52 | this.asyncLocalStorage.run(context, callback); 53 | } 54 | } 55 | 56 | OpenFeature.setTransactionContextPropagator( 57 | new AsyncLocalStorageTransactionContext() 58 | ); 59 | ``` 60 | 61 | ### Set Transaction Context 62 | 63 | Setting transaction context will vary based on the language. In JavaScript, it may look like this: 64 | 65 | ```typescript 66 | /** 67 | * This example is based on an express middleware. 68 | */ 69 | use(req: Request, _res: Response, next: NextFunction) { 70 | OpenFeature.setTransactionContext({ targetingKey: req.user.id }, () => { 71 | next(); 72 | }); 73 | } 74 | ``` 75 | 76 | > NOTE: Setting the same property multiple times will override the previous value. 77 | 78 | ### Get Transaction Context 79 | 80 | Getting transaction context happens automatically in the OpenFeature client before flag evaluation occurs. 81 | 82 | ### Context Merge Order 83 | 84 | Transaction context merging should happen between global context and client context. This provides a reasonable balance between context inheritance and the ability to override context properties. 85 | 86 | ```mermaid 87 | flowchart LR 88 | global("API (global)") 89 | transaction("Transaction") 90 | client("Client") 91 | invocation("Invocation") 92 | hook("Before Hooks") 93 | global --> transaction 94 | transaction --> client 95 | client --> invocation 96 | invocation --> hook 97 | ``` 98 | 99 | ### Implementation 100 | 101 | - [X] [Example implementation in Node](https://github.com/open-feature/js-sdk/pull/212) 102 | - [ ] Example implementation in Golang 103 | 104 | [thread-local]: https://docs.oracle.com/javase/8/docs/api/java/lang/ThreadLocal.html 105 | [go-context]: https://pkg.go.dev/context 106 | [async-hooks]: https://nodejs.org/api/async_hooks.html 107 | [otel-baggage-poc]: https://github.com/open-feature/playground/pull/142 108 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenFeature Enhancement Proposals(OFEP) 2 | 3 | This repository serves as a focal point for research and experimental work. 4 | It also enables the creation of discussions, proposals and ideation through issues. 5 | 6 | We use OFEP: OpenFeature Enhancement proposals, which comes from the lineage of [PEP](https://peps.python.org/pep-0001/) much like the Kubernetes project uses [KEP](https://github.com/kubernetes/enhancements/blob/master/keps/README.md) and Open-Telemetry project uses [OTEP](https://github.com/open-telemetry/oteps/blob/main/README.md). 7 | 8 | ## Should I create an OFEP? 9 | 10 | You should create an OFEP for anything that: 11 | 12 | - a blog post would be written about after its release (eg. [Client-side Feature Flagging](https://openfeature.dev/blog/catering-to-the-client-side)) 13 | - requires multiple parties/owners participating to complete (eg. Client-side Feature Flagging [Specification & SDKs]) 14 | - requires significant effort or modifications to OpenFeature (eg. something that would take 10 person-weeks to implement, introduce or redesign a system component, or introduces Specification changes) 15 | - impacts the UX or operation of OpenFeature substantially such that engineers using OpenFeature will need retraining 16 | - users will notice and come to rely on 17 | - impacts multiple implementations or languages 18 | 19 | It is unlikely an enhancement if it is: 20 | - rephrasing, grammatical fixes, typos, etc 21 | - bug fixes 22 | - refactoring code 23 | - adding error messages or events 24 | - a thing that affects only a single language or implementation 25 | 26 | **Note**: The above lists are intended only as examples and are not meant to be exhaustive. If you don't know whether a change requires an OFEP, please feel free ping someone listed in [sdk-maintainers and cloud-native maintainers](https://github.com/orgs/open-feature/teams) (or) ask in the [CNCF OpenFeature Slack channel](https://cloud-native.slack.com/archives/C0344AANLA1). If you are new, you can create a CNCF Slack account [here](https://slack.cncf.io/). 27 | 28 | ### OFEP Scope 29 | 30 | While OFEPs are intended for "significant" changes, we recommend trying to keep each OFEP's scope as small as makes sense (eg. on broader scale, mentioning the category of the proposal). A general rule of thumb is that if the core functionality proposed could still provide value without a particular piece, then that piece should be removed from the proposal and used instead as an *example* (and, ideally, given its own OFEP!). 31 | 32 | ## How to start with writing an OFEP? 33 | 34 | - First, [create an issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/creating-an-issue) using the [Enhancement Proposal](https://github.com/open-feature/ofep/issues/new?labels=OFEP&projects=&template=Proposal.yaml&title=%5BProposal%5D+) template. 35 | - Fill in the template. Put care into the details: It is important to present convincing motivation, demonstrate an understanding of the design's impact, and honestly assess the drawbacks and potential alternatives. 36 | 37 | ## Discussing the OFEP 38 | 39 | - As soon as the above-mentioned issue is created, potential reviewers (based on the `category of proposal` specified in the issue) would be automatically asked for review. This is done as a preventive measure to avoid long-winded and open-ended discussions in the early design phase of the proposal but the OFEP author should use their discretion here. 40 | - The OFEP author may choose any place for discussions but needs to be linked to the issue. The suggested ones include continuing in the same GitHub issue or creating a thread on [Slack](https://cloud-native.slack.com/archives/C0344AANLA1) mentioning the issue. 41 | - The OFEP authors are responsible for collecting community feedback on an OFEP before submitting it as a proposal for review. 42 | 43 | ## Submitting the OFEP and life-cycle 44 | 45 | - Once the idea of the proposal is reviewed by the assigned reviewers, the OFEP author can then reference a Pull Request to the issue containing the proposal. 46 | - For adding the OFEP as a Pull Request, first, [fork](https://help.github.com/en/articles/fork-a-repo) this [repo](https://github.com/open-feature/ofep). 47 | - Copy [`OFEP-template.md`](./OFEP-template.md) to `OFEP/my-title.md`, where `my-title` is a title relevant to your proposal. If you want to attach any image to the OFEP, add that image to the `OFEP/images` folder and attach it to the OFEP in the format `![label](images/image-name.png "label")`. 48 | - Fill in the template and please take care of the details as followed while creating the issue for Enhancement Proposal. 49 | - The initial `status` of an OFEP should be in `drafting` or `pending for review` stage. 50 | - An OFEP is `approved` when atleast two/three reviewers github-approve the PR but this surely depends on its nature. The OFEP is then merged. 51 | - If an OFEP is `rejected` or `withdrawn`, the PR is closed. Note that these OFEPs submissions are still recorded, as GitHub retains both the discussion and the proposal, even if the branch is later deleted. 52 | - If an OFEP discussion becomes long, or the OFEP then goes through a major revision, the next version of the OFEP can be posted as a new PR, which references the old PR. The old PR is then closed. This makes OFEP review easier to follow and participate in. 53 | 54 | ### Automerging-flow of an OFEP Pull Request 55 | 56 | - Approved OFEP Pull Requests receive the automerge label. 57 | - A 3-day waiting period starts for objection raising. 58 | - A comment is also posted on the Pull Request stating the same. 59 | - Objections lead to the removal of the automerge label. 60 | - No objections result in auto-merging by [Mergify](https://mergify.com). 61 | 62 | ## Implementing the OFEP 63 | 64 | Some accepted OFEPs represent vital features that need to be implemented right away. Other accepted OFEPs can represent features that can wait until a community member decides to implement the functionality. Every accepted OFEP has an associated issue tracking its implementation in the specific repository. 65 | 66 | The author of an OFEP is not obligated to implement it. Of course, the OFEP author (like any other developer) is welcome to post an implementation for review after the OFEP has been accepted. 67 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/copy_files.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | from distutils.dir_util import copy_tree 4 | 5 | # Get the root directory of the repository 6 | root_directory = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) 7 | 8 | # Define the source and destination directories 9 | source_directory = os.path.join(root_directory, 'OFEP') 10 | destination_directory = os.path.join(root_directory, 'docs', 'source') 11 | source_image_directory = os.path.join(source_directory, 'images') 12 | destination_image_directory = os.path.join(destination_directory, 'images') 13 | 14 | # Create the destination directory if it doesn't exist 15 | if not os.path.exists(destination_directory): 16 | os.makedirs(destination_directory) 17 | 18 | # Copy the ofeps from OFEP directory to docs/source 19 | for filename in os.listdir(source_directory): 20 | if filename.endswith('.md') and filename.find('template')==-1 and filename.find('index')==-1: 21 | source_file_path = os.path.join(source_directory, filename) 22 | destination_file_path = os.path.join(destination_directory, filename) 23 | if os.path.exists(destination_file_path): 24 | os.remove(destination_file_path) 25 | shutil.copy2(source_file_path, destination_file_path) 26 | print(f"Copied {source_file_path} to {destination_file_path}") 27 | 28 | print("Copying completed.") 29 | 30 | 31 | # copy the images subdirectory from OFEP to docs/source 32 | if not os.path.exists(destination_image_directory): 33 | os.makedirs(destination_image_directory) 34 | 35 | 36 | copy_tree(source_image_directory, destination_image_directory) 37 | -------------------------------------------------------------------------------- /docs/generate_index.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | from datetime import datetime 4 | 5 | # Define a function to parse frontmatter data 6 | def parse_frontmatter(md_content): 7 | frontmatter = {} 8 | match = re.match(r'---(.*?)---', md_content, re.DOTALL) 9 | if match: 10 | frontmatter_text = match.group(1) 11 | lines = frontmatter_text.strip().split('\n') 12 | for line in lines: 13 | key, value = map(str.strip, line.split(':', 1)) 14 | frontmatter[key] = value 15 | return frontmatter 16 | 17 | # Initialize data structures to store categorized OFEPs 18 | status_to_ofeps = { 19 | 'Approved': [], 20 | 'Rejected': [], 21 | 'Withdrawn': [], 22 | } 23 | 24 | root_directory = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) 25 | 26 | # Define the source and script directories 27 | source_directory = os.path.join(root_directory, 'OFEP') 28 | script_directory = os.path.join(root_directory, 'docs', 'source') 29 | 30 | if not os.path.exists(script_directory): 31 | os.makedirs(script_directory) 32 | 33 | # Get the list of OFEP files in the source directory 34 | ofep_files = [file for file in os.listdir(source_directory) if file.endswith('.md') and file != 'OFEP-template.md'] 35 | 36 | # Generate the toctree for index.rst 37 | content = 'OpenFeature Enhancement Proposals\n========================================\n .. toctree::\n :titlesonly:\n :maxdepth: 1\n :hidden:\n :caption: OFEP Documentation\n\n ' 38 | 39 | content += '\n '.join([ofep_file[:-3] for ofep_file in ofep_files]) 40 | 41 | # Iterate through the files in the OFEP directory 42 | index_content = "\n\nOFEP Index\n===========\n\n" 43 | 44 | for filename in os.listdir(source_directory): 45 | if filename.endswith('.md') and filename != 'OFEP-template.md': 46 | with open(os.path.join(source_directory, filename), 'r') as file: 47 | md_content = file.read() 48 | frontmatter = parse_frontmatter(md_content) 49 | ofep_status = frontmatter.get('status', 'Unknown') 50 | 51 | if ofep_status in status_to_ofeps: 52 | date_str = frontmatter.get('date', '1970-01-01') 53 | date_obj = datetime.strptime(date_str, '%Y-%m-%d') 54 | authors_list = ', '.join([author.strip() for author in re.findall(r'\[(.*?)\]', frontmatter.get('authors', ''))]) 55 | tags_list = ', '.join([tag.strip() for tag in re.findall(r'\[(.*?)\]', frontmatter.get('tags', ''))]) 56 | status_to_ofeps[ofep_status].append((date_obj, frontmatter['title'], authors_list, tags_list, filename)) 57 | 58 | # Sort OFEPs in each category by date 59 | for status, ofeps in status_to_ofeps.items(): 60 | status_to_ofeps[status] = sorted(ofeps, key=lambda x: x[0], reverse=True) 61 | 62 | # Generate the index content with the table 63 | index_content += "\n" 64 | 65 | for status, ofeps in status_to_ofeps.items(): 66 | index_content += f"{status}\n{'=' * len(status)}\n\n" 67 | index_content += ".. list-table::\n" 68 | index_content += " :header-rows: 1\n" 69 | index_content += " :widths: auto\n" 70 | index_content += "\n" 71 | index_content += " * - Last Modified\n" 72 | index_content += " - Title\n" 73 | index_content += " - Authors\n" 74 | index_content += " - Tags\n" 75 | 76 | for date_obj, title, authors_list, tags_list, filename in ofeps: 77 | formatted_date = date_obj.strftime('%dth %b %Y') # Format as "25th May 2023" 78 | title_link = f"`{title} <{filename.replace('.md', '.html')}>`_" 79 | index_content += f" * - {formatted_date}\n" 80 | index_content += f" - {title_link}\n" 81 | index_content += f" - {authors_list}\n" 82 | index_content += f" - {tags_list}\n" 83 | 84 | index_content += "\n" 85 | 86 | # Write the index to a file 87 | index_path = os.path.join(script_directory, 'index.rst') 88 | if os.path.exists(index_path): 89 | os.remove(index_path) 90 | with open(index_path, 'w') as index_file: 91 | index_file.write(content) 92 | index_file.write(index_content) 93 | 94 | print(f"Index generated successfully at {index_path}.") -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx==7.1.2 2 | sphinx_rtd_theme==1.3.0 3 | myst-parser==2.0.0 -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | import os 6 | import sys 7 | 8 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) 9 | parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) 10 | 11 | # -- Project information ----------------------------------------------------- 12 | project = 'OpenFeature Enhancement Proposal' 13 | copyright = '2023 OpenFeature is a Cloud Native Computing Foundation incubating project | All Rights Reserved' 14 | author = 'Mihir Mittal' 15 | release = '1' 16 | 17 | # -- Extension setup --------------------------------------------------------- 18 | extensions = [ 19 | "sphinx.ext.duration", 20 | "sphinx.ext.doctest", 21 | "sphinx.ext.autodoc", 22 | "sphinx.ext.autosummary", 23 | "sphinx.ext.intersphinx", 24 | "myst_parser", 25 | ] 26 | 27 | # -- Custom script execution ------------------------------------------------- 28 | index_gen_path = os.path.join(parent_dir,'docs', 'generate_index.py') 29 | copy_files_path = os.path.join(parent_dir,'docs', 'copy_files.py') 30 | 31 | if not os.path.exists('_build'): 32 | os.makedirs('_build') 33 | 34 | python_executable = 'python' 35 | os.system(f'{python_executable} {copy_files_path}') 36 | os.system(f'{python_executable} {index_gen_path}') 37 | 38 | 39 | source_suffix = ['.rst', '.md'] 40 | 41 | 42 | templates_path = ['_templates'] 43 | exclude_patterns = [] 44 | 45 | html_theme = 'sphinx_rtd_theme' 46 | -------------------------------------------------------------------------------- /prior-art/api-comparision.md: -------------------------------------------------------------------------------- 1 | # API Comparison 2 | 3 | The goal of this research is to make it easy to quickly compare feature flag 4 | vendor SDK API's in order to help define the OpenFeature spec. 5 | 6 | ## Flag Return Types 7 | 8 | | Provider x supported types | Unleash | Flagsmith | LaunchDarkly | Split | CloudBees | Harness | 9 | | -------------------------- | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | 10 | | boolean | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | :heavy_check_mark: | :heavy_check_mark: | 11 | | string | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | 12 | | numeric | | | :heavy_check_mark: | | :heavy_check_mark: | :heavy_check_mark: | 13 | | JSON | :heavy_check_mark: | | | :heavy_check_mark: | | :heavy_check_mark: | 14 | 15 | ### Boolean 16 | 17 |
18 | Java 19 | 20 | ```java 21 | /** 22 | * Unleash 23 | * 24 | * SDK Repo: https://github.com/Unleash/unleash-client-java 25 | */ 26 | boolean isEnabled(String toggleName) 27 | boolean isEnabled(String toggleName, UnleashContext context) 28 | boolean isEnabled(String toggleName, UnleashContext context, boolean defaultSetting) 29 | boolean isEnabled(String toggleName, BiFunction fallbackAction) 30 | boolean isEnabled(String toggleName, UnleashContext context, BiFunction fallbackAction) 31 | 32 | /** 33 | * Flagsmith 34 | * 35 | * SDK Repo: https://github.com/Flagsmith/flagsmith-java-client 36 | */ 37 | boolean hasFeatureFlag(String featureId) 38 | boolean hasFeatureFlag(String featureId, FeatureUser user) 39 | boolean hasFeatureFlag(String featureId, FlagsAndTraits flagsAndTraits) 40 | 41 | /** 42 | * LaunchDarkly 43 | * 44 | * SDK Repo: https://github.com/launchdarkly/java-server-sdk 45 | */ 46 | boolean boolVariation(String featureKey, LDUser user, boolean defaultValue) 47 | // Response also contains evaluation details 48 | EvaluationDetail boolVariationDetail(String featureKey, LDUser user, boolean defaultValue) 49 | 50 | /** 51 | * Split 52 | * 53 | * SDK Repo: https://github.com/splitio/java-client 54 | * 55 | * NOTE: Not supported 56 | * NOTE: Split always returns a string but using the values "on" and "off" is a common practice. 57 | */ 58 | 59 | /** 60 | * CloudBees Rollout 61 | * 62 | * SDK Repo: N/A 63 | * 64 | * NOTE: Flags are configured as code and contain default values. 65 | * 66 | */ 67 | 68 | boolean isEnabled(): boolean; 69 | boolean isEnabled(Context context): boolean; 70 | 71 | // example usage 72 | public class Flags implements RoxContainer { 73 | public RoxFlag videoChat = new RoxFlag(); 74 | } 75 | Flags flags = new Flags(); 76 | Rox.register("test-namespace", flags); 77 | flags.videoChat.enabled 78 | 79 | // Dynamic API 80 | Rox.dynamicApi.isEnabled('system.reportAnalytics', false); 81 | Rox.dynamicApi.isEnabled('system.reportAnalytics', false, context); 82 | 83 | /** 84 | * Harness 85 | * 86 | * SDK Repo: https://github.com/harness/ff-java-server-sdk 87 | */ 88 | boolean boolVariation(String key, Target target, boolean defaultValue) 89 | ``` 90 | 91 |
92 | 93 |
94 | NodeJS 95 | 96 | ```typescript 97 | /** 98 | * Unleash 99 | * 100 | * SDK Repo: https://github.com/Unleash/unleash-client-node 101 | */ 102 | isEnabled(name: string, context?: Context, fallbackFunction?: FallbackFunction): boolean; 103 | isEnabled(name: string, context?: Context, fallbackValue?: boolean): boolean; 104 | isEnabled(name: string, context: Context = {}, fallback?: FallbackFunction | boolean): boolean; 105 | 106 | /** 107 | * Flagsmith 108 | * 109 | * SDK Repo: https://github.com/Flagsmith/flagsmith-nodejs-client 110 | */ 111 | hasFeature(key: string): Promise 112 | hasFeature(key: string, userId: string): Promise 113 | 114 | /** 115 | * LaunchDarkly 116 | * 117 | * SDK Repo: https://github.com/launchdarkly/node-client-sdk 118 | * 119 | * Note: TS typings assign LDFlagValue = any; 120 | * Note: variation values can be defined as boolean, number, or string. Documentation suggests casting: https://docs.launchdarkly.com/sdk/client-side/node-js#getting-started 121 | */ 122 | 123 | variation( 124 | key: string, 125 | user: LDUser, 126 | defaultValue: LDFlagValue, 127 | callback?: (err: any, res: LDFlagValue) => void 128 | ): Promise 129 | // Response also contains evaluation details 130 | variationDetail( 131 | key: string, 132 | user: LDUser, 133 | defaultValue: LDFlagValue, 134 | callback?: (err: any, res: LDEvaluationDetail) => void 135 | ): Promise; 136 | 137 | /** 138 | * Split 139 | * 140 | * SDK Repo: https://github.com/splitio/javascript-client 141 | * 142 | * NOTE: Not supported 143 | * NOTE: TS typings assign Treatment = string; 144 | * NOTE: Split always returns a string but using the values "on" and "off" is a common practice. 145 | */ 146 | 147 | /** 148 | * CloudBees Rollout 149 | * 150 | * SDK Repo: N/A 151 | * 152 | * NOTE: Flags are configured as code and contain default values. 153 | * 154 | */ 155 | 156 | isEnabled(context?: unknown): boolean; 157 | 158 | // example usage 159 | const flags = { 160 | videoChat: new Rox.Flag() 161 | }; 162 | 163 | Rox.register('test-namespace', flags); 164 | flags.videoChat.isEnabled() 165 | flags.videoChat.isEnabled(context) 166 | 167 | /** 168 | * Harness 169 | * 170 | * SDK Repo: https://github.com/harness/ff-nodejs-server-sdk 171 | */ 172 | boolVariation( 173 | identifier: string, 174 | target: Target, 175 | defaultValue: boolean = true, 176 | ): Promise 177 | ``` 178 | 179 |
180 | 181 | ### String 182 | 183 |
184 | Java 185 | 186 | ```java 187 | /** 188 | * Unleash 189 | * 190 | * SDK Repo: https://github.com/Unleash/unleash-client-java 191 | * 192 | * NOTE: Variants can contain string, csv, or JSON 193 | */ 194 | Variant getVariant(final String toggleName) 195 | Variant getVariant(final String toggleName, final UnleashContext context) 196 | Variant getVariant(final String toggleName, final Variant defaultValue) 197 | Variant getVariant(final String toggleName, final UnleashContext context, final Variant defaultValue) 198 | 199 | /** 200 | * Flagsmith 201 | * 202 | * SDK Repo: https://github.com/Flagsmith/flagsmith-java-client 203 | */ 204 | String getFeatureFlagValue(String featureId) 205 | String getFeatureFlagValue(String featureId, FeatureUser user) 206 | String getFeatureFlagValue(String featureId, FlagsAndTraits flagsAndTraits) 207 | 208 | /** 209 | * LaunchDarkly 210 | * 211 | * SDK Repo: https://github.com/launchdarkly/java-server-sdk 212 | */ 213 | String stringVariation(String featureKey, LDUser user, boolean defaultValue) 214 | // Response also contains evaluation details 215 | EvaluationDetail stringVariationDetail(String featureKey, LDUser user, boolean defaultValue) 216 | 217 | /** 218 | * Split 219 | * 220 | * SDK Repo: https://github.com/splitio/java-client 221 | */ 222 | String getTreatment(String key, String split) 223 | String getTreatment(String key, String split, Map attributes) 224 | String getTreatment(Key key, String split, Map attributes) 225 | 226 | /** 227 | * CloudBees Rollout 228 | * 229 | * SDK Repo: N/A 230 | * 231 | * NOTE: Flags are configured as code and contain default values. 232 | * 233 | */ 234 | 235 | String getValue(); 236 | String getValue(Context context); 237 | 238 | // example usage: 239 | public class Flags implements RoxContainer{ 240 | public RoxVariant titleColors = new RoxVariant("White", new String[] {"White", "Blue", "Green", "Yellow"}); 241 | } 242 | 243 | Flags flags = new Flags(); 244 | Rox.register("test-namespace", flags); 245 | 246 | flags.titleColors.getValue() 247 | flags.titleColors.getValue(context) 248 | 249 | // Dynamic API 250 | Rox.dynamicApi.value('ui.textColor', "red"); 251 | Rox.dynamicApi.value('ui.textColor', "red", context); 252 | 253 | /** 254 | * Harness 255 | * 256 | * SDK Repo: https://github.com/harness/ff-java-server-sdk 257 | */ 258 | String stringVariation(String key, Target target, String defaultValue) 259 | ``` 260 | 261 |
262 | 263 |
264 | NodeJS 265 | 266 | ```typescript 267 | /** 268 | * Unleash 269 | * 270 | * SDK Repo: https://github.com/Unleash/unleash-client-node 271 | * NOTE: Variants can contain string, csv, or JSON 272 | */ 273 | getVariant(name: string, context: Context = {}, fallbackVariant?: Variant): Variant 274 | 275 | /** 276 | * Flagsmith 277 | * 278 | * SDK Repo: https://github.com/Flagsmith/flagsmith-nodejs-client 279 | */ 280 | getValue(key: string): Promise; 281 | getValue(key: string, userId: string): Promise; 282 | 283 | /** 284 | * LaunchDarkly 285 | * 286 | * SDK Repo: https://github.com/launchdarkly/node-client-sdk 287 | * 288 | * Note: TS typings assign LDFlagValue = any; 289 | * Note: variation values can be defined as boolean, number, or string. Documentation suggests casting: https://docs.launchdarkly.com/sdk/client-side/node-js#getting-started 290 | */ 291 | variation( 292 | key: string, 293 | user: LDUser, 294 | defaultValue: LDFlagValue, 295 | callback?: (err: any, res: LDFlagValue) => void 296 | ): Promise 297 | // Response also contains evaluation details 298 | variationDetail( 299 | key: string, 300 | user: LDUser, 301 | defaultValue: LDFlagValue, 302 | callback?: (err: any, res: LDEvaluationDetail) => void 303 | ): Promise; 304 | 305 | /** 306 | * Split 307 | * 308 | * SDK Repo: https://github.com/splitio/javascript-client 309 | * 310 | * Note: TS typings assign Treatment = string; 311 | * NOTE: Split always returns a string. 312 | * 313 | */ 314 | getTreatment(key: SplitKey, splitName: string, attributes?: Attributes): Treatment 315 | getTreatment(splitName: string, attributes?: Attributes): Treatment 316 | 317 | /** 318 | * CloudBees Rollout 319 | * 320 | * SDK Repo: N/A 321 | * 322 | * NOTE: Flags are configured as code and contain default values. 323 | * 324 | */ 325 | 326 | getValue(context?: unknown): string; 327 | 328 | // example usage 329 | const flags = { 330 | titleColors: new RoxString('White', ['White', 'Blue', 'Green', 'Yellow']) 331 | }; 332 | 333 | Rox.register('test-namespace', flags); 334 | flags.titleColors.value(); 335 | 336 | /** 337 | * Harness 338 | * 339 | * SDK Repo: https://github.com/harness/ff-nodejs-server-sdk 340 | */ 341 | function stringVariation( 342 | identifier: string, 343 | target: Target, 344 | defaultValue: boolean = '', 345 | ): Promise; 346 | ``` 347 | 348 |
349 | 350 | ### Integers and Floats 351 | 352 |
353 | Java 354 | 355 | ```java 356 | /** 357 | * Unleash 358 | * 359 | * SDK Repo: https://github.com/Unleash/unleash-client-java 360 | * 361 | * NOTE: Not supported; strings can be parsed into numeric values 362 | */ 363 | 364 | /** 365 | * Flagsmith 366 | * 367 | * SDK Repo: https://github.com/Flagsmith/flagsmith-java-client 368 | * 369 | * NOTE: Not supported; strings can be parsed into numeric values 370 | */ 371 | 372 | /** 373 | * LaunchDarkly 374 | * 375 | * SDK Repo: https://github.com/launchdarkly/java-server-sdk 376 | */ 377 | int intVariation(String featureKey, LDUser user, int defaultValue) 378 | double doubleVariation(String featureKey, LDUser user, double defaultValue) 379 | // Response also contains evaluation details 380 | EvaluationDetail intVariationDetail(String featureKey, LDUser user, int defaultValue) 381 | EvaluationDetail doubleVariationDetail(String featureKey, LDUser user, double defaultValue) 382 | 383 | /** 384 | * Split 385 | * 386 | * SDK Repo: https://github.com/splitio/java-client 387 | * 388 | * NOTE: Not supported; strings can be parsed into numeric values 389 | */ 390 | 391 | /** 392 | * CloudBees Rollout 393 | * 394 | * SDK Repo: N/A 395 | */ 396 | 397 | int getValue(); 398 | int getValue(Context context); 399 | double getValue(); 400 | double getValue(Context context); 401 | 402 | // example usage 403 | public class Container implements RoxContainer { 404 | public final RoxInt titleSize = new RoxInt(5, new int[]{ 8, 13 }); 405 | public final RoxDouble specialNumber = new RoxDouble(3.14, new double[]{ 2.71, 0.577 }); 406 | } 407 | 408 | Container flags = new Container(); 409 | Rox.register("test-namespace", flags); 410 | flags.titleSize.getValue(); 411 | flags.specialNumber.getValue(); 412 | 413 | // Dynamic API 414 | Rox.dynamicApi.getNumber('ui.textSize', 12); 415 | Rox.dynamicApi.getNumber('ui.textColor', 18, context); 416 | 417 | /** 418 | * Harness 419 | * 420 | * SDK Repo: https://github.com/harness/ff-java-server-sdk 421 | */ 422 | double numberVariation(String key, Target target, int defaultValue) 423 | ``` 424 | 425 |
426 | 427 |
428 | NodeJS 429 | 430 | ```typescript 431 | /** 432 | * Unleash 433 | * 434 | * SDK Repo: https://github.com/Unleash/unleash-client-node 435 | * 436 | * NOTE: Not supported; Strings can be parsed into numeric values. 437 | */ 438 | 439 | /** 440 | * Flagsmith 441 | * 442 | * SDK Repo: https://github.com/Flagsmith/flagsmith-nodejs-client 443 | */ 444 | getValue(key: string): Promise; 445 | getValue(key: string, userId: string): Promise; 446 | 447 | /** 448 | * LaunchDarkly 449 | * 450 | * SDK Repo: https://github.com/launchdarkly/node-client-sdk 451 | * 452 | * Note: TS typings assign LDFlagValue = any; 453 | * Note: variation values can be defined as boolean, number, or string. Documentation suggests casting: https://docs.launchdarkly.com/sdk/client-side/node-js#getting-started 454 | */ 455 | variation( 456 | key: string, 457 | user: LDUser, 458 | defaultValue: LDFlagValue, 459 | callback?: (err: any, res: LDFlagValue) => void 460 | ): Promise 461 | // Response also contains evaluation details 462 | variationDetail( 463 | key: string, 464 | user: LDUser, 465 | defaultValue: LDFlagValue, 466 | callback?: (err: any, res: LDEvaluationDetail) => void 467 | ): Promise; 468 | 469 | /** 470 | * Split 471 | * 472 | * SDK Repo: https://github.com/splitio/javascript-client 473 | * 474 | * NOTE: Not supported; Strings can be parsed into numeric values. 475 | * NOTE: TS typings assign Treatment = string; 476 | */ 477 | 478 | /** 479 | * CloudBees Rollout 480 | * 481 | * SDK Repo: N/A 482 | * 483 | * NOTE: Flags are configured as code and contain default values. 484 | * 485 | */ 486 | 487 | getValue(context?: unknown): number; 488 | 489 | // example usage 490 | const flags = { 491 | titleSize: new RoxNumber(12, [12, 14, 18, 24]) 492 | }; 493 | 494 | Rox.register('test-namespace', flags); 495 | flags.titleSize.value(); 496 | 497 | /** 498 | * Harness 499 | * 500 | * SDK Repo: https://github.com/harness/ff-nodejs-server-sdk 501 | */ 502 | function numberVariation( 503 | identifier: string, 504 | target: Target, 505 | defaultValue: boolean = 1.0, 506 | ): Promise; 507 | ``` 508 | 509 |
510 | 511 | ### JSON 512 | 513 |
514 | Java 515 | 516 | ```java 517 | /** 518 | * Unleash 519 | * 520 | * SDK Repo: https://github.com/Unleash/unleash-client-java 521 | * 522 | * NOTE: Variants can contain string, csv, or JSON 523 | * 524 | */ 525 | Variant getVariant(final String toggleName) 526 | Variant getVariant(final String toggleName, final UnleashContext context) 527 | Variant getVariant(final String toggleName, final Variant defaultValue) 528 | Variant getVariant(final String toggleName, final UnleashContext context, final Variant defaultValue) 529 | 530 | /** 531 | * Flagsmith 532 | * 533 | * SDK Repo: https://github.com/Flagsmith/flagsmith-java-client 534 | * 535 | * NOTE: Not supported 536 | */ 537 | 538 | /** 539 | * LaunchDarkly 540 | * 541 | * SDK Repo: https://github.com/launchdarkly/java-server-sdk 542 | * 543 | * Note: TS typings assign LDFlagValue = any; 544 | * Note: variation values can be defined as boolean, number, or string; JSON structures can be encoded as strings. 545 | */ 546 | 547 | /** 548 | * Split 549 | * 550 | * SDK Repo: https://github.com/splitio/java-client 551 | */ 552 | SplitResult getTreatmentWithConfig(String key, String split) 553 | SplitResult getTreatmentWithConfig(String key, String split, Map attributes) 554 | SplitResult getTreatmentWithConfig(Key key, String split, Map attributes) 555 | 556 | /** 557 | * CloudBees Rollout 558 | * 559 | * SDK Repo: N/A 560 | * 561 | * NOTE: Not supported 562 | */ 563 | 564 | /** 565 | * Harness 566 | * 567 | * SDK Repo: https://github.com/harness/ff-java-server-sdk 568 | */ 569 | JsonObject jsonVariation(String key, Target target, JsonObject defaultValue) 570 | ``` 571 | 572 |
573 | 574 |
575 | NodeJS 576 | 577 | ```typescript 578 | /** 579 | * Unleash 580 | * 581 | * SDK Repo: https://github.com/Unleash/unleash-client-node 582 | * 583 | * NOTE: Variants support JSON, CSV, and String payloads types, but the SDK seems to only have "String" enumerated: https://github.com/Unleash/unleash-client-node/blob/5da3b2980da63bd899619a3e558cab7874c2dbe0/src/variant.ts#L7 584 | */ 585 | getVariant(name: string, context: Context, fallbackVariant?: Variant): Variant 586 | 587 | /** 588 | * Flagsmith 589 | * 590 | * SDK Repo: https://github.com/Flagsmith/flagsmith-nodejs-client 591 | * 592 | * NOTE: Not supported; JSON structures can be encoded as strings. 593 | */ 594 | 595 | /** 596 | * LaunchDarkly 597 | * 598 | * SDK Repo: https://github.com/launchdarkly/node-client-sdk 599 | * 600 | * Note: TS typings assign LDFlagValue = any; 601 | * Note: variation values can be defined as boolean, number, or string; JSON structures can be encoded as strings. 602 | */ 603 | 604 | /** 605 | * Split 606 | * 607 | * SDK Repo: https://github.com/splitio/javascript-client 608 | * 609 | * NOTE: TreatmentWithConfig contains a "config" property, which is a stringified version of the configuration JSON object 610 | * 611 | */ 612 | getTreatmentWithConfig(key: SplitKey, splitName: string, attributes?: Attributes): TreatmentWithConfig, 613 | getTreatmentWithConfig(splitName: string, attributes?: Attributes): TreatmentWithConfig, 614 | 615 | /** 616 | * CloudBees Rollout 617 | * 618 | * SDK Repo: N/A 619 | * 620 | * NOTE: Not supported; JSON structures can be encoded as strings. 621 | * 622 | */ 623 | 624 | /** 625 | * Harness 626 | * 627 | * SDK Repo: https://github.com/harness/ff-nodejs-server-sdk 628 | */ 629 | function jsonVariation( 630 | identifier: string, 631 | target: Target, 632 | defaultValue: object = {}, 633 | ): Promise>; 634 | ``` 635 | 636 |
637 | 638 | ## Attributes for flag evaluation 639 | 640 | Most feature flag implementations support the inclusion of contextual attributes as the basis for differential evaluation of feature flags based on rules that can be defined in the flag system. Below is a summary of how various vendors allow these attributes to be supplied in flag evaluation, the types they support, and the terminology they use. 641 | 642 | | Provider x attribute concepts | Unleash | Flagsmith | LaunchDarkly | Split | CloudBees | Harness | 643 | | ----------------------------- | ----------------------- | --------------------- | --------------------------- | --------------------------- | --------------------- | ------------------ | 644 | | nomenclature | "context", "properties" | "user", "traits" | "user", "attributes" | "attributes" | "context" | "target" | 645 | | standard attributes | :heavy_check_mark: | | :heavy_check_mark: | | :heavy_check_mark: | | 646 | | custom attributes | :heavy_check_mark: | :heavy_check_mark: | | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | 647 | | nested custom attributes | | | | | :heavy_check_mark: | | 648 | | user key | `userId` | `identifier` | `key` | `key`\* | `distinct_id` | `identifier`\* | 649 | | custom attribute types | string | boolean,string,number | boolean,number,string,Array | boolean,number,string,Array | boolean,number,string | number,string\*\* | 650 | 651 | \* not a field, provided as distinct parameter in flag evaluation 652 | 653 | \*\* possibly more? Documentation is unclear 654 | 655 |
656 | Unleash 657 | 658 | The unleash context is an object used to store data for use in flag evaluation. A number of fields are defined by default, and additional custom fields can be specified. Some fields within the context are "static", provided at initialization, immutable for the lifetime of the application, while others are dynamic and can change with each evaluation. 659 | 660 | ``` 661 | interface Context { 662 | string environment?; // static 663 | string appName?; // static 664 | Date currentTime?; 665 | string userId?; 666 | string sessionId?; 667 | string remoteAddress?; 668 | Map properties?; 669 | } 670 | ``` 671 | 672 | see: https://docs.getunleash.io/user_guide/unleash_context 673 | 674 |
675 | 676 |
677 | Flagsmith 678 | 679 | Flagsmith associates "traits" (attributes) on the user object using that user's userId. Traits can be booleans, numbers, or strings. 680 | 681 | ### v1 SDK: 682 | 683 | ``` 684 | flagsmith.setTrait(userId, key, value); 685 | 686 | ``` 687 | 688 | see: https://docs.flagsmith.com/basic-features/managing-identities#identity-traits 689 | 690 | ### v2-beta SDK: 691 | 692 | ``` 693 | await this.client.getIdentityFlags( 694 | identifier, 695 | traits // this is a "dictionary"/key-value map 696 | ) 697 | ``` 698 | 699 |
700 | 701 |
702 | LaunchDarkly 703 | 704 | The "user" object must be passed in every flag evaluation, and defines a key to identify a users, as well as a number of pre-defined optional properties which can be used in flag evaluation logic. Additional custom properties can be specified in the nested `custom` property. 705 | 706 | ``` 707 | interface LDUser { 708 | string key; 709 | string secondary?; 710 | string name?; 711 | string firstName?; 712 | string lastName?; 713 | string email?; 714 | string avatar?; 715 | string ip?; 716 | string country; 717 | boolean anonymous?; 718 | Map> custom? 719 | privateAttributeNames?: Array; 720 | } 721 | ``` 722 | 723 | see: https://docs.launchdarkly.com/home/users/attributes 724 | 725 |
726 | 727 |
728 | Split 729 | 730 | Attributes are custom data that can be used in targeting rules, which can be optionally supplied during flag evaluation. 731 | 732 | Note: the user `key` identifies a user and is a distinct, required parameter in the split SDKs. 733 | 734 | see: https://help.split.io/hc/en-us/articles/360020793231-Target-with-custom-attributes 735 | 736 |
737 | 738 |
739 | CloudBees 740 | 741 | Properties are arbitrary data which can be used in flag evaluation. Cloudbees FM defines a few standard properties (`app_release`, `language`, `platform`, `screen_height` and `screen_width`), and allows custom properties to be defined. Note that the Cloudbees SDK requires the application author to explicitly define custom attributes. The context object passed at flag evaluation time can be used to compute properties. 742 | 743 | ``` 744 | // define a new property, using the context object to set it's value. 745 | Rox.setCustomBooleanProperty('my-new-prop', (context) => { 746 | return context.myPropValue; 747 | }); 748 | ``` 749 | 750 | ``` 751 | // pass the context into flag evaluation: 752 | var context = { myPropValue = true }; 753 | Rox.dynamicApi.isEnabled('my-flag', false, context); 754 | ``` 755 | 756 | see: https://docs.cloudbees.com/docs/cloudbees-feature-management/latest/feature-releases/custom-properties 757 | 758 |
759 | 760 |
761 | Harness 762 | 763 | A "target" is a conceptual user whose experience can be differentially impacted by "targeting rules". Harness defines a few standard properties (an `identifier`, a `name`, and an `anonymous` boolean), as well as a map of arbitrary custom attributes. The target is a required parameter for every flag evaluation. 764 | 765 | ``` 766 | Target { 767 | string identifier; 768 | string name; 769 | boolean anonymous; 770 | Map attributes; 771 | } 772 | ``` 773 | 774 | see: https://ngdocs.harness.io/article/xf3hmxbaji-targeting-users-with-flags#on_request_check_for_condition_and_serve_variation 775 | 776 |
777 | -------------------------------------------------------------------------------- /prior-art/existing-landscape.md: -------------------------------------------------------------------------------- 1 | 2 | # Feature Flag Management - API/SDK Landscape Research 3 | 4 | ## Goal 5 | 6 | The goal of this research is to document the APIs and SDKs of existing feature flag 7 | management tools in order to identity patterns that should be considered when 8 | defining the OpenFeature spec. 9 | 10 | > NOTE: This research focuses primarily on the server-side NodeJS SDKs 11 | 12 | ## Unleash 13 | 14 | Unleash has documented the expected behavior of the client SDKs [here](https://github.com/Unleash/client-specification). 15 | 16 | ```typescript 17 | // isEnabled(name: string, context?: Context, fallbackFunction?: FallbackFunction): boolean; 18 | // isEnabled(name: string, context?: Context, fallbackValue?: boolean): boolean; 19 | const featureEnabled = unleash.isEnabled("test-release"); 20 | ``` 21 | 22 | ### Context 23 | 24 | Context in Unleash is used to make feature state evaluation at runtime. The noteworthy properties are: 25 | 26 | - userId 27 | - sessionId 28 | - remoteAddress 29 | - environment 30 | - appName 31 | - properties (additional context as key-value pairs) 32 | 33 | The context interface for NodeJS can be found 34 | [here](https://github.com/Unleash/unleash-client-node/blob/main/src/context.ts). 35 | 36 | ### Variant 37 | 38 | Variants are extensions to feature flags in Unleash. They allow Unleash 39 | administrators to define customized response payloads (which can be further 40 | customized using context). For example, if a feature flag called 'banner-color' 41 | is enabled, the variant could return a string value specifying which color the 42 | banner should be, using, for instance, the user's role. 43 | 44 | The variant interfaces for NodeJS can be found [here](https://github.com/Unleash/unleash-client-node/blob/main/src/variant.ts). 45 | 46 | 47 | ```typescript 48 | // getVariant(name: string, context?: Context, fallbackVariant?: Variant): Variant; 49 | const colorVariant = unleash.getVariant("banner-color"); 50 | // NOTE: Values are always strings but can stringified JSON 51 | const color = colorVariant.enabled && colorVariant.payload?.value || "#0000FF"; 52 | ``` 53 | 54 | ### Flag evaluation 55 | 56 | Unleash uses 57 | [strategies](https://github.com/Unleash/unleash-client-node/blob/main/src/strategy/strategy.ts) 58 | to do local flag evaluation in their server-side SDKs. A number of strategies come out of the box 59 | and it's possible to create custom strategies that are registered in the client 60 | constructor. Flag configurations are periodically polled and persisted in memory 61 | by default. 62 | 63 | > NOTE: Client-side SDKs require the [Unleash 64 | > Proxy](https://github.com/Unleash/unleash-proxy), which handles flag 65 | > evaluation on a server. 66 | ### Key findings 67 | 68 | - Evaluation logic is handled in the SDK and some clients support synchronous 69 | operations. 70 | - The fallback argument accepting a function provides developers the opportunity 71 | for more sophisticated logic when an error occurs. 72 | - Variants do not support integer values. 73 | - Some SDKs, like 74 | [Java](https://docs.getunleash.io/sdks/java_sdk#step-4-provide-unleash-context), 75 | support injected/implicit context, for example, provided via request-scoped bean. 76 | - Initial synchronization can impact app start-up time. 77 | - Invalid flag IDs behave like a disabled feature. 78 | 79 | 80 | ## Flagsmith 81 | 82 | The following is an example of a basic flag evaluation where the expected return 83 | type is a boolean. 84 | 85 | ```typescript 86 | // hasFeature(key: string): Promise; 87 | // hasFeature(key: string, userId: string): Promise; 88 | const featureEnabled = await flagsmith.hasFeature("test-feature"); 89 | ``` 90 | 91 | It's also possible to return values associated with a feature flag. These values 92 | are returned despite the state of the flag. 93 | 94 | ```typescript 95 | // getValue(key: string): Promise; 96 | // getValue(key: string, userId: string): Promise; 97 | const value = await flagsmith.getValue("test-feature"); 98 | ``` 99 | 100 | ### Identity management 101 | 102 | Users are automatically created the first time they're referenced in a flag 103 | evaluation. Traits can be associated with a user and are persisted 104 | server-side. It doesn't appear that traits can be removed via the SDK. 105 | 106 | ### Segments 107 | 108 | Segments can be applied to a feature, allowing Flagsmith administrators to 109 | override default behavior. This is done by evaluating traits associated with a 110 | user. 111 | 112 | ### Flag evaluation 113 | 114 | Flagsmith performs flag evaluation server-side and results can be cached if 115 | desired. This service is written in Python and can be found 116 | [here](https://github.com/Flagsmith/flagsmith-engine). 117 | 118 | ### Key findings 119 | 120 | - Evaluation logic is handed on the server and called via REST. However, the v2 121 | SDK allows developers to choose between remote or local evaluation. The 122 | pro/cons list can be found 123 | [here](https://docs.flagsmith.com/next/clients/overview#pros-cons-and-caveats). 124 | - Identities need to maintained outside of the flag evaluation. 125 | - Values are always returned, even if the flag is disabled. 126 | - Invalid flag IDs behave like a disabled feature and return a null value (In 127 | NodeJS). 128 | 129 | ## LaunchDarkly 130 | 131 | The method names and signatures vary between languages. The following is how a 132 | flag is evaluated in NodeJS: 133 | 134 | ```typescript 135 | /** 136 | * variation( 137 | * key: string, 138 | * user: LDUser, 139 | * defaultValue: LDFlagValue, 140 | * callback?: (err: any, res: LDFlagValue) => void 141 | * ): Promise; 142 | */ 143 | const value = await ld.variation("test-feature", { key: "test-user" }, false); 144 | ``` 145 | 146 | And this is that same flag in Java: 147 | 148 | ```java 149 | /* 150 | Other methods include intVariation, doubleVariation, stringVariation, and jsonValueVariation 151 | */ 152 | boolean value = client.boolVariation("test-feature", user, false); 153 | ``` 154 | 155 | ## Users 156 | 157 | A user key always needs to be defined when performing a flag evaluation. 158 | Anonymous users still need a key and should have the property `anonymous` set to 159 | true. More information can be found 160 | [here](https://docs.launchdarkly.com/sdk/features/user-config). 161 | 162 | The user interface accepts the following properties: 163 | - key 164 | - secondary 165 | - name 166 | - firstName 167 | - lastName 168 | - email 169 | - avatar 170 | - ip 171 | - country 172 | - anonymous 173 | - custom 174 | 175 | The user interface for NodeJS can be found 176 | [here](https://github.com/launchdarkly/node-server-sdk/blob/master/index.d.ts#L533). 177 | 178 | ### Flag evaluation 179 | 180 | LaunchDarkly performs local flag evaluation in the server SDKs. Configuration 181 | updates can be streamed using Server Sent Events (SSE) or by polling and 182 | persisted in memory by default. 183 | 184 | Client SDKs evaluations are performed server-side and the results are cached. 185 | More information can be found 186 | [here](https://docs.launchdarkly.com/sdk/concepts/client-side-server-side#flag-evaluations). 187 | 188 | > NOTE: Client-side SDKs do not subscribe to real-time updates until the 189 | > `.on('change')` method is called. 190 | ### Key findings 191 | - There's a `variationDetails` method that returns evaluation and error details. 192 | - Remote debugging option captures additional evaluation details. 193 | - The return type from the variation method is `any` (In NodeJS). 194 | - Flag variation is defined during flag creation and cannot be modified later. 195 | - Private user attributes can be [disabled from 196 | analytics](https://docs.launchdarkly.com/home/users/attributes#configuring-private-attribute-settings-in-your-sdk) 197 | while still being available for local flag evaluation. 198 | - It's possible to update the `processor` used by the client to [read from a 199 | file](https://docs.launchdarkly.com/sdk/features/flags-from-files). 200 | 201 | ## Split 202 | 203 | The following is an example of a basic flag evaluation. Split refers to the 204 | response type as a treatment and it's always a string. 205 | 206 | ```typescript 207 | // getTreatment(splitName: string, attributes?: Attributes): Treatment 208 | // getTreatment(key: SplitKey, splitName: string, attributes?: Attributes): Treatment 209 | const treatment = client.getTreatment("test-user", "test-feature"); 210 | 211 | // Custom attributes can also be used during treatment evaluation 212 | const treatmentUsingAttributes = client.getTreatment("test-user", "test-feature", { groups: ["internal"] }); 213 | ``` 214 | 215 | It's also possible to attach a configuration to a treatment. This can be either 216 | a key/value pair or JSON. The following example shows how you can access the 217 | config associated with a treatment. 218 | 219 | ```typescript 220 | // getTreatmentWithConfig(splitName: string, attributes?: Attributes): TreatmentWithConfig 221 | // getTreatmentWithConfig(key: SplitKey, splitName: string, attributes?: Attributes): TreatmentWithConfig 222 | const treatmentWithConfig = client.getTreatmentWithConfig("test-user", "test-feature"); 223 | // Treatment value 224 | console.log(treatmentWithConfig.treatment); 225 | // Config is either a string or null 226 | console.log(treatmentWithConfig.config) 227 | ``` 228 | 229 | ### Treatments 230 | 231 | Treatment values are the primary response when performing a flag evaluation. 232 | These values are always strings and default to `on` and `off`. However, it's 233 | possible to create a up to 20 treatments per split (feature flag) and the 234 | default values can be changed. 235 | 236 | There are also [reserved words](https://help.split.io/hc/en-us/articles/360020525112-Edit-treatments#about-reserved-words) that can't be used on a treatment. 237 | ### Attributes 238 | 239 | Attributes can be used to build targeting rules for feature flags. They are maps 240 | that are passed into the `getTreatment` method. Supported values are: strings, 241 | numbers, dates, booleans, and sets. 242 | 243 | ### Flag evaluation 244 | 245 | Split performs local flag evaluation in both the client and server 246 | SDKs. Configuration updates can be streamed using Server Sent Events (SSE) or by 247 | polling and persisted in memory by default. A 248 | [proxy](https://help.split.io/hc/en-us/articles/4415960499213-Split-Proxy) can 249 | be used to reduce the connection latency and number of connections directly 250 | accessing Split. 251 | 252 | It's also possible to use a [Split 253 | Evaluator](https://help.split.io/hc/en-us/articles/360020037072-Split-Evaluator) 254 | for languages without an official SDK. This exposes a REST API using the NodeJS SDK behind the 255 | scenes. 256 | 257 | ### Key findings 258 | 259 | - SDKs support a [localhost 260 | mode](https://help.split.io/hc/en-us/articles/360020564931-Node-js-SDK#localhost-mode). 261 | - Treatment return values are string, so string a comparison is used to 262 | determine the state of the flag. 263 | - Additional metrics can be collected by sending events via the `track` method. 264 | - Traffic splitting is deterministic based on a hash of the user id. 265 | - When a visitor is assigned a treatment for a split, an impression is 266 | created. Registering a `logImpression` callback provides a detailed overview 267 | of the impression, attributes, and available metadata. 268 | - A 269 | [manager](https://help.split.io/hc/en-us/articles/360020564931-Node-js-SDK#manager) 270 | can be created with the same factory used to create the client. The manager 271 | contains methods that can be used to understand what flags exist and how they're 272 | configured. 273 | 274 | ## CloudBees Feature Management 275 | 276 | SDK behaviour is documented [here](https://docs.cloudbees.com/docs/cloudbees-feature-management/latest/getting-started/). 277 | 278 | ```typescript 279 | const booleanFlag = new Rox.Flag() // Flag evaluation defaults to false 280 | const enabled = booleanFlag.isEnabled() 281 | const colorOptions = new Rox.RoxString('White', ['White', 'Blue', 'Green', 'Yellow']) // specify a default flag value and different possible variants 282 | const colorVariant = colorOptions.getValue() 283 | ``` 284 | 285 | ### Flag names 286 | 287 | Flag names are determined either by reflection of the variable name, or via a string parameter via the [dynamic API](https://docs.cloudbees.com/docs/cloudbees-feature-management/latest/feature-flags/dynamic-api) 288 | 289 | ```typescript 290 | // Use the variable name to determine the flag name 291 | const myBooleanFlag = new Rox.Flag() 292 | let enabled = myBooleanFlag.isEnabled() 293 | 294 | // Or pass it as a string parameter 295 | enabled = Rox.dynamicApi.isEnabled('myBooleanFlag', false); 296 | ``` 297 | 298 | ### Available types 299 | 300 | The following flag types are [available](https://docs.cloudbees.com/docs/cloudbees-feature-management/latest/feature-flags/creating-feature-flags): 301 | 302 | * Boolean 303 | * String 304 | * Number (integer) 305 | 306 | ### Context 307 | 308 | You can specify any additional evaluation context when evaluating a flag 309 | 310 | ```typescript 311 | const enabled = booleanFlag.isEnabled({ // any arbitrary evaluation context can be passed into the evaluation call 312 | email: 'my@email.com', 313 | uid: 123 314 | }) 315 | ``` 316 | 317 | ### Flag evaluation 318 | 319 | Flag evaluation is performed client-side for all SDKs. A ruleset for the given environment is downloaded by the SDK upon startup (and periodically thereafter). Any flag configuration changes are pushed to the SDK via [Server Side Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events). 320 | 321 | ### Key findings 322 | 323 | * All flag evaluation is performed locally within the SDK 324 | * Flags are created/defined in code and specify a default value. If the flag is not enabled ('targeting' is on), then the default value is returned. 325 | * Split flag evaluation (eg, Blue/Green flags) are deterministic based on a unique and persistent user ID for each client. 326 | * An arbitrary evaluation context is available for every flag evaluation 327 | * Flag names are either determined from the variable name (reflection) or [dynamically](https://docs.cloudbees.com/docs/cloudbees-feature-management/latest/feature-flags/dynamic-api) from a string parameter 328 | --------------------------------------------------------------------------------