Skip to content

Commit 410055f

Browse files
muhammad-ali-eclaudeDeepak-Kesavanathul-rsgaya3-zipstack
authored
UN-3291 [FIX] Migrate Flipt v1 to v2 and fix gRPC client bugs for feature flags (#1808)
* UN-3291 [FIX] Migrate Flipt v1 to v2 and fix gRPC client bugs for feature flags Migrate Docker dev essentials Flipt from v1.34.0 to v2.3.1 with file-based storage, and fix three gRPC client bugs introduced in PR #1665 that caused all feature flag evaluations to silently return False. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * UN-3291 [DOCS] Add docstring for check_feature_flag_variant() Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * UN-3291 [FIX] Remove deprecated namespace_key param from new evaluate_variant method Since evaluate_variant is a new method, there's no need to include the deprecated namespace_key parameter. The namespace is already configured at the client level via UNSTRACT_FEATURE_FLAG_NAMESPACE. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * UN-3291 [FIX] Remove unused namespace_key param from check_feature_flag_variant New method doesn't need the deprecated parameter since the namespace is configured at the FliptClient level via UNSTRACT_FEATURE_FLAG_NAMESPACE. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Deepak K <89829542+Deepak-Kesavan@users.noreply.github.com> Co-authored-by: Athul <89829560+athul-rs@users.noreply.github.com> Co-authored-by: Gayathri <142381512+gaya3-zipstack@users.noreply.github.com>
1 parent 5ebb6fb commit 410055f

File tree

5 files changed

+169
-12
lines changed

5 files changed

+169
-12
lines changed

docker/docker-compose-dev-essentials.yaml

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -92,18 +92,14 @@ services:
9292
- "host.docker.internal:host-gateway"
9393

9494
feature-flag:
95-
image: flipt/flipt:v1.34.0 # Dated(05/01/2024) Latest stable version. Ref:https://github.com/flipt-io/flipt/releases
95+
image: docker.flipt.io/flipt/flipt:v2.3.1
9696
container_name: unstract-flipt
9797
restart: unless-stopped
9898
ports: # Forwarded to available host ports
9999
- "8082:8080" # REST API port
100100
- "9005:9000" # gRPC port
101-
# https://www.flipt.io/docs/configuration/overview#environment-variables)
102-
# https://www.flipt.io/docs/configuration/overview#configuration-parameters
103-
env_file:
104-
- ./essentials.env
105-
environment:
106-
FLIPT_CACHE_ENABLED: true
101+
volumes:
102+
- flipt_data:/var/opt/flipt
107103
labels:
108104
- traefik.enable=true
109105
- traefik.http.routers.feature-flag.rule=Host(`feature-flag.unstract.localhost`)

docker/sample.essentials.env

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,6 @@ MINIO_ROOT_PASSWORD=minio123
1010
MINIO_ACCESS_KEY=minio
1111
MINIO_SECRET_KEY=minio123
1212

13-
# Use encoded password Refer : https://docs.sqlalchemy.org/en/20/core/engines.html#escaping-special-characters-such-as-signs-in-passwords
14-
FLIPT_DB_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}?sslmode=disable"
15-
1613
QDRANT_USER=unstract_vector_dev
1714
QDRANT_PASS=unstract_vector_pass
1815
QDRANT_DB=unstract_vector_db

unstract/flags/src/unstract/flags/client/flipt.py

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def evaluate_boolean(
6868
flag_key=flag_key, entity_id=entity_id, context=context or {}
6969
)
7070

71-
return result.value if hasattr(result, "value") else False
71+
return result.enabled if hasattr(result, "enabled") else False
7272

7373
except Exception as e:
7474
logger.error(f"Error evaluating flag {flag_key} for entity {entity_id}: {e}")
@@ -81,6 +81,69 @@ def evaluate_boolean(
8181
except Exception as e:
8282
logger.error(f"Error closing Flipt client: {e}")
8383

84+
def evaluate_variant(
85+
self,
86+
flag_key: str,
87+
entity_id: str | None = "unstract",
88+
context: dict | None = None,
89+
) -> dict:
90+
"""Evaluate a variant feature flag for a given entity.
91+
92+
Args:
93+
flag_key: The key of the feature flag to evaluate
94+
entity_id: The ID of the entity for which to evaluate the flag
95+
context: Additional context for evaluation
96+
97+
Returns:
98+
dict: {"match": bool, "variant_key": str, "variant_attachment": str,
99+
"segment_keys": list[str]}
100+
Returns empty dict with match=False if service unavailable or error.
101+
"""
102+
default_result = {
103+
"match": False,
104+
"variant_key": "",
105+
"variant_attachment": "",
106+
"segment_keys": [],
107+
}
108+
if not self.service_available:
109+
logger.warning("Flipt service not available, returning default for all flags")
110+
return default_result
111+
112+
client = None
113+
try:
114+
client = FliptGrpcClient(opts=self.grpc_opts)
115+
116+
result = client.evaluate_variant(
117+
flag_key=flag_key, entity_id=entity_id, context=context or {}
118+
)
119+
120+
return {
121+
"match": result.match if hasattr(result, "match") else False,
122+
"variant_key": (
123+
result.variant_key if hasattr(result, "variant_key") else ""
124+
),
125+
"variant_attachment": (
126+
result.variant_attachment
127+
if hasattr(result, "variant_attachment")
128+
else ""
129+
),
130+
"segment_keys": (
131+
list(result.segment_keys) if hasattr(result, "segment_keys") else []
132+
),
133+
}
134+
135+
except Exception as e:
136+
logger.error(
137+
f"Error evaluating variant flag {flag_key} for entity {entity_id}: {e}"
138+
)
139+
return default_result
140+
finally:
141+
if client:
142+
try:
143+
client.close()
144+
except Exception as e:
145+
logger.error(f"Error closing Flipt client: {e}")
146+
84147
def list_feature_flags(self, namespace_key: str | None = None) -> dict:
85148
"""List all feature flags in a namespace.
86149

unstract/flags/src/unstract/flags/feature_flag.py

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,98 @@ def check_feature_flag_status(
4242
context=context or {},
4343
)
4444

45-
return bool(result.enabled)
45+
return bool(result)
4646
except Exception:
4747
return False
48+
49+
50+
def check_feature_flag_variant(
51+
flag_key: str,
52+
entity_id: str = "unstract",
53+
context: dict[str, str] | None = None,
54+
) -> dict:
55+
"""Check a variant feature flag and return its evaluation result.
56+
57+
Evaluates a Flipt variant flag and returns the full evaluation response.
58+
The function first checks whether the flag is enabled before calling
59+
Flipt's variant evaluation API.
60+
61+
Args:
62+
flag_key: The flag key of the feature flag.
63+
entity_id: An identifier for the evaluation entity. Used by Flipt
64+
for consistent percentage-based rollout hashing only — it does
65+
NOT influence segment constraint matching.
66+
context: Key-value pairs matched against Flipt segment constraints.
67+
Keys must correspond exactly to the constraint property names
68+
configured in Flipt. For example, if a segment has a constraint
69+
on property "organization_id", pass
70+
``{"organization_id": "org_123"}``. Defaults to None.
71+
72+
Returns:
73+
dict with the following fields:
74+
75+
- **enabled** (bool): Whether the flag is enabled in Flipt.
76+
- **match** (bool): Whether the entity matched a segment rule.
77+
- **variant_key** (str): The key of the matched variant (empty
78+
string if no match).
79+
- **variant_attachment** (str): JSON string attached to the variant
80+
(empty string if no match). Parse with ``json.loads()`` to get
81+
structured data.
82+
- **segment_keys** (list[str]): Segment keys that were matched.
83+
84+
Result interpretation:
85+
- ``enabled=False`` → Flag is disabled or not found in Flipt.
86+
All other fields are at their defaults.
87+
- ``enabled=True, match=True`` → The entity's context matched a
88+
segment rule and a variant was assigned. ``variant_key`` and
89+
``variant_attachment`` contain the assigned values.
90+
- ``enabled=True, match=False`` → The flag is on but no segment
91+
rule matched the provided context. This typically means Flipt
92+
is missing Segments and/or Rules for this flag, or the context
93+
keys/values don't satisfy any segment constraint.
94+
95+
Note:
96+
Variant flags in Flipt require three things to be configured for
97+
``match=True``: **Variants** (the possible values), **Segments**
98+
(constraint-based groups), and **Rules** (which link segments to
99+
variants). If any of these are missing, evaluation returns
100+
``match=False``.
101+
102+
Example::
103+
104+
import json
105+
106+
result = check_feature_flag_variant(
107+
flag_key="extraction_engine",
108+
context={"organization_id": "org_123"},
109+
)
110+
if result["enabled"] and result["match"]:
111+
config = json.loads(result["variant_attachment"])
112+
engine = config["engine"]
113+
"""
114+
default_result = {
115+
"enabled": False,
116+
"match": False,
117+
"variant_key": "",
118+
"variant_attachment": "",
119+
"segment_keys": [],
120+
}
121+
try:
122+
client = FliptClient()
123+
124+
# Check enabled status first
125+
flags = client.list_feature_flags()
126+
if not flags.get("flags", {}).get(flag_key, False):
127+
return default_result
128+
129+
# Flag is enabled, evaluate variant
130+
result = client.evaluate_variant(
131+
flag_key=flag_key,
132+
entity_id=entity_id,
133+
context=context or {},
134+
)
135+
result["enabled"] = True
136+
137+
return result
138+
except Exception:
139+
return default_result
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,10 @@
11
"""Flipt gRPC protobuf definitions."""
2+
3+
# Pre-register the Timestamp well-known type in the protobuf descriptor pool.
4+
# Required because flipt_simple_pb2.py and evaluation_simple_pb2.py declare
5+
# a dependency on google/protobuf/timestamp.proto in their serialized descriptors.
6+
# Without this, AddSerializedFile() fails with KeyError (pure-Python) or TypeError (C/upb).
7+
# In the backend, this happens to work because Google Cloud libraries (google-cloud-storage,
8+
# etc.) import timestamp_pb2 as a side effect during Django startup. Workers don't have
9+
# that implicit dependency, so we must be explicit.
10+
from google.protobuf import timestamp_pb2 as _timestamp_pb2 # noqa: F401

0 commit comments

Comments
 (0)