Repository navigation
Conversation
Add the add-typed-flag-values OpenSpec change. It defines the SDK API for typed flag values read from the /flags?v=3 response: - typed value and details accessors for boolean, string, number and object, on the evaluateFlags() snapshot and on client SDKs; - resolution without coercion, and how the caller default relates to the flag default; - one evaluation-details object with OpenFeature reasons and error codes and the contract's flag metadata keys; - legacy accessors and $feature_flag_called stay unchanged; - behaviour against older servers, bootstrap, overrides and local evaluation, and how OpenFeature providers wrap the accessors. Proposal only: the change is not applied or archived yet. The design lists the decisions still to settle.
A config version 2 experiment rule carries no experiment ID, so no /flags?v=3 record carries metadata.experiment_id. Remove it from the flag_metadata key list, the experiment split scenario and the design assumption.
Config version 2 holdouts are rule-local and have no ID, so no /flags?v=3 record carries metadata.holdout_id. A holdout result is identified by its rule ID and the holdout reason code. Remove the key from the flag_metadata key list and the design assumption.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This adds the OpenSpec change
add-typed-flag-values. It defines the SDK API for typed feature flag values and evaluation details, read from the/flags?v=3response: what an application developer calls, what comes back, and what the SDK does behind it.Inputs:
contracts/feature_flag_rules_v2(contract 2.3.1): the v3 producer schema, the reader fixtures, the OpenFeature mapping and the legacy rendering table. This change does not touch the wire./flags?v=3) and feat(flags): accept and evaluate string, number and object rules v2 values posthog#107733 (string, number and object values).Files:
openspec/changes/add-typed-flag-values/proposal.mdopenspec/changes/add-typed-flag-values/design.md: the decisions in more detail, scored on developer experience, OpenFeature compatibility and migrationopenspec/changes/add-typed-flag-values/tasks.mdopenspec/changes/add-typed-flag-values/specs/typed-flag-values/spec.md(new capability)openspec/changes/add-typed-flag-values/specs/evaluate-flags/spec.md(delta)openspec/changes/add-typed-flag-values/specs/feature-flag-called-tracker/spec.md(delta)Decisions to settle
Each decision has the assumption the spec takes, the main alternative, and the reason. The
Dnumbers match the headings indesign.md, which has the full reasoning.$feature_flag_calledon access, snapshot reuse incapture).evaluateFlags()snapshot only, with no typed single-flag client methods. Client SDKs put them on the client and read the loaded flags.client.getBooleanValue(key, default, distinctId, options)and the rest to server clients.evaluateFlagsfor one key at the same cost.nullvalue counts as present (it renders asfalse). The typed boolean accessor returns a presentfalseas is (the flag default wins), and returns the caller default fornull, failed and missing flags. Non-boolean accessors treatfalseandnullas "no value": caller default, no error.isEnabled(key, { defaultValue })treat a v3nullas "no value".nullvalue asenabled: false. Visible consequence: for anullflag default,isEnabled(k, { defaultValue: true })isfalsebutgetBooleanValue(k, true)istrue.valueprojection for number and object flagsgetFlagand enablement keep the legacy rendering. Number and object values aretrue, and the value is available through the payload accessor and the typed accessors.nullisfalse. No new untypedgetValue.getFlagreturn the typed value, or add an untyped accessor.getFlagis typedboolean | string | undefinedin several SDKs and feeds$feature/<key>,$active_feature_flagsand bootstrap export.key,value,variant,reason(OpenFeature reason string),error_code(OpenFeature codes only, see D6),error_message,flag_metadata.flag_metadatais a scalar map with the contract's keys (id,version,config_version,reason_code,condition_index,rule_type,rule_id,variant_key), using wire names in every language. It leaves out the payload and the reason description.variantis left out when an error code is set.reasonand a typed metadata object); orflagKey, camelCase metadata keys, or PostHog-specific error codes.reason_code, and every SDK has one shape.FLAG_NOT_FOUND,PARSE_ERROR,TYPE_MISMATCH,INVALID_CONTEXT,PROVIDER_NOT_READY,GENERAL); the PostHog code stays inflag_metadata.reason_code. For version 1 flags, a failed record isERROR,flag_disabledisDISABLED, atrueor string value isTARGETING_MATCH, anything elseDEFAULT. This is what the providers report today. An unknown config version 2 code isUNKNOWN.DEPENDENCY_ERROR, or a per-code table for version 1 reason codes, added to the contract.$feature_flag_called$feature_flag_responseis the legacy rendering (truefor number and object values,falsefornull), never the caller default. A type mismatch is not reported as$feature_flag_error. Attribution properties and any dedupe-key change come with$experiment_exposure.$feature_flag_response.variant ?? enabled, config version 1, no rule context). Typed number and object reads return the caller default withTYPE_MISMATCH, and the error message names the likely cause. The SDK logs one diagnostic per client. No public field marks the state.STALE, or rebuilding values from payloads.error_codeanderror_message; rebuilding values from payloads would be coercion.metadata.payload: nullfor those flags). Users who model config as variant plus payload keep their code. To move, they recreate the flag as an object flag and read it withgetObjectValue;details.variantstill names the arm.getFeatureFlagResultposthog.getStringValue('banner-copy', 'Welcome')). Types are boolean, string, number and object, plus integer and float accessors where the language separates them.getFeatureFlagResultstays separate and unchanged;get<Type>Detailsis the details form.posthog.getStringFlag(...)), or growingFeatureFlagResultinto the details form.FeatureFlagResultwould give one object two value channels.getBooleanValueon a multivariate flag isTYPE_MISMATCH, whileisFeatureEnabledreturnstrue. Typed accessors do not parse variant strings and do not fall back to payloads.PROVIDER_NOT_READY.FLAG_NOT_FOUND.CACHEDandoverride, with empty metadata. The bootstrap format does not change, so number and object flags cannot be bootstrapped as typed values.bootstrap.featureFlags.getFeatureFlagPayloadandgetFeatureFlagResultnow.Before and after
Python (server snapshot):
Node (server snapshot):
Browser JS (client):
Migration story
$feature_flag_calledand dedupe together, so a code base can mix them freely.details.variantonly to know the arm.isFeatureEnabled(k)becomesgetBooleanValue(k, false). Two differences: a multivariate flag isTYPE_MISMATCH, and a config version 2 flag with anulldefault returns your default.getFeatureFlag(k)becomesgetStringValue(k, 'control'). It returns the same variant key, and returns your default instead offalseorundefinedwhen nothing matches.Differences from posthog-python#1031
reasoncode,condition_index,description)reason_codeandcondition_indexinflag_metadataflag_metadatamap of the contract's keys, without the payloadvariantwhen an error code is setconfig_version1TYPE_MISMATCHmessage names the causePlacement, resolution rules, error codes, legacy rendering, payload synthesis and event behaviour match.
Worth looking at first
v=3. The harness compliance testflags_request_uses_v2_query_paramstill assertsv=2for server SDKs, so it needs a change before SDKs adopt this.Process
tasks.mdsections 2 to 4). The acceptance feature and the README index entry are part of that step.openspec validate add-typed-flag-values --strictandopenspec validate --specs --strictpass with OpenSpec 1.4.1.🤖 Generated with Claude Code