Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 13 additions & 13 deletions .github/workflows/compatibility.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,67 +25,67 @@ jobs:
repository: Flagsmith/flagsmith-python-client
ref: main
language: python
tags: ''
tags: core or events or local-evaluation
- name: nodejs
repository: Flagsmith/flagsmith-nodejs-client
ref: main
language: nodejs
tags: ''
tags: core or events or local-evaluation
- name: java
repository: Flagsmith/flagsmith-java-client
ref: main
language: java
tags: ''
tags: core or events or local-evaluation
- name: dotnet
repository: Flagsmith/flagsmith-dotnet-client
ref: main
language: dotnet
tags: ''
tags: core or events or local-evaluation
- name: go
repository: Flagsmith/flagsmith-go-client
ref: main
language: go
tags: ''
tags: core or events or local-evaluation
- name: ruby
repository: Flagsmith/flagsmith-ruby-client
ref: main
language: ruby
tags: ''
tags: core or events or local-evaluation
- name: php
repository: Flagsmith/flagsmith-php-client
ref: main
language: php
tags: ''
tags: core or events or local-evaluation
- name: rust
repository: Flagsmith/flagsmith-rust-client
ref: main
language: rust
tags: ''
tags: core or events or local-evaluation
- name: elixir
repository: Flagsmith/flagsmith-elixir-client
ref: main
language: elixir
tags: ''
tags: core or events or local-evaluation
- name: js-client
repository: Flagsmith/flagsmith-js-client
ref: main
language: nodejs
tags: ''
tags: core or events
- name: ios
repository: Flagsmith/flagsmith-ios-client
ref: main
language: swift
tags: ''
tags: core or events
- name: android
repository: Flagsmith/flagsmith-kotlin-android-client
ref: main
language: kotlin
tags: ''
tags: core or events
- name: flutter
repository: Flagsmith/flagsmith-flutter-client
ref: main
language: dart
tags: ''
tags: core or events
uses: ./.github/workflows/run-spec.yml
with:
spec-repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ Each release also pins the Flagsmith version in `compose.yml` that it was verifi
- `Create the SDK client with events enabled` receives the same two, then the base URL of
Flagsmith's events API, and returns a client that sends events there. Only `@events`
scenarios use it, so an SDK without event tracking can leave it out.
- `Create the SDK client with local evaluation` receives the same two as `Create the SDK client`,
and returns a client that evaluates flags locally, with its environment already loaded. Only
`@local-evaluation` scenarios use it, so an SDK without local evaluation can leave it out.
- A `When` step receives the client, then the step's placeholders in order, then its data table.
A `<key> | value` table arrives as a map, e.g. `{"plan": "premium", "age": 30}`.
- Values are JSON values. Pass flag and trait values through with their types intact.
Expand Down
36 changes: 36 additions & 0 deletions features/local__get_environment_flags.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
@local-evaluation
Feature: Get environment flags with local evaluation
A server-side SDK evaluates the flags for its environment against the environment document,
serving the same flags Flagsmith would.

Background:
Given a Flagsmith environment
And the SDK evaluates flags locally

Scenario: Environment flags are disabled while a flag they depend on is disabled
Given the environment has the following features:
| feature | enabled | value |
| payments | false | null |
| checkout | true | "v2" |
| search | true | null |
| autocomplete | true | 5 |
And feature "checkout" depends on feature "payments"
And feature "autocomplete" depends on feature "search"
When the application gets the environment flags
Then the application receives the following flags:
| feature | enabled | value |
| payments | false | null |
| checkout | false | "v2" |
| search | true | null |
| autocomplete | true | 5 |

Scenario: Environment flags ignore segments that need an identity
Given the environment has the following features:
| feature | enabled | value |
| banner | true | "hello" |
And a segment "premium_users" where trait "plan" equals "premium"
And segment "premium_users" overrides feature "banner" with enabled false and value "hello premium user"
When the application gets the environment flags
Then the application receives the following flags:
| feature | enabled | value |
| banner | true | "hello" |
1 change: 1 addition & 0 deletions runner/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ addopts = "--strict-markers -p no:cacheprovider"
markers = [
"core: Behaviour every SDK must implement",
"events: Tracking and exposure events, powering experimentation",
"local-evaluation: Evaluating flags in the SDK, against the environment document",
Comment thread
khvn26 marked this conversation as resolved.
Resolved
]
11 changes: 11 additions & 0 deletions runner/sdk_spec/flagsmith_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,17 @@ def create_segment_override(
},
)

def create_flag_dependency(
self, environment: Environment, feature: str, prerequisite: str
) -> None:
"""Disables the feature in the environment unless the prerequisite is enabled."""
self._request(
"POST",
f"/environments/{environment.api_key}/features/"
f"{environment.features[feature].id}/dependencies/"
f"{environment.features[prerequisite].id}/",
)

def get_identity_traits(self, environment: Environment, identifier: str) -> dict[str, Any]:
identities = self._request(
"GET", f"/environments/{environment.api_key}/identities/?q={identifier}"
Expand Down
34 changes: 28 additions & 6 deletions runner/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ class ScenarioState:
environment: Environment | None = None
client: str | None = None
events: bool = False
local_evaluation: bool = False
result: Any = None


Expand Down Expand Up @@ -96,7 +97,12 @@ def sdk(pytestconfig: pytest.Config) -> Iterator[SDK]:
if not SDK_COMMAND:
pytest.exit("Set SDK_COMMAND to the command that starts the SDK harness", returncode=4)
sdk = SDK(SDK_COMMAND)
required = [CREATE_CLIENT, CREATE_CLIENT_WITH_EVENTS, *SDK_STEPS]
required = [
CREATE_CLIENT,
CREATE_CLIENT_WITH_EVENTS,
CREATE_CLIENT_WITH_LOCAL_EVALUATION,
*SDK_STEPS,
]
implemented = [step for step in required if step in sdk.implemented_steps]
# Write through the terminal reporter, so the line is shown even with output captured.
terminal = pytestconfig.pluginmanager.get_plugin("terminalreporter")
Expand Down Expand Up @@ -154,6 +160,8 @@ def require_environment(state: ScenarioState) -> Environment:
# A second client, for scenarios that are about the events an SDK sends. Events are off by
# default in every SDK, and the events API is not derived from the Flagsmith API URL.
CREATE_CLIENT_WITH_EVENTS = "Create the SDK client with events enabled"
# A client evaluating flags against the environment document, for server-side SDKs only.
CREATE_CLIENT_WITH_LOCAL_EVALUATION = "Create the SDK client with local evaluation"
SDK_STEPS: list[str] = []
PLACEHOLDER = re.compile(r'("?)\{(\w+)\}\1')

Expand All @@ -166,11 +174,12 @@ def sdk_client(state: ScenarioState, sdk: SDK) -> str:
args = [environment.server_side_key, f"{FLAGSMITH_SDK_URL.rstrip('/')}/api/v1/"]
if state.events:
args.append(f"{EVENTS_SDK_URL.rstrip('/')}/")
state.client = sdk.step(
CREATE_CLIENT_WITH_EVENTS if state.events else CREATE_CLIENT,
args,
returns_client=True,
)
step = CREATE_CLIENT_WITH_EVENTS
elif state.local_evaluation:
step = CREATE_CLIENT_WITH_LOCAL_EVALUATION
else:
step = CREATE_CLIENT
state.client = sdk.step(step, args, returns_client=True)
return state.client


Expand Down Expand Up @@ -237,6 +246,12 @@ def sdk_sends_events(state: ScenarioState, flagsmith: FlagsmithAdmin) -> None:
state.events = True


@given("the SDK evaluates flags locally")
def sdk_evaluates_flags_locally(state: ScenarioState) -> None:
"""Asks for a client with local evaluation, which the runner creates for the first SDK step."""
state.local_evaluation = True


@given("the environment has the following features:")
def environment_features(
state: ScenarioState, flagsmith: FlagsmithAdmin, datatable: list[list[str]]
Expand Down Expand Up @@ -291,6 +306,13 @@ def segment_override(
)


@given(parsers.parse('feature "{feature}" depends on feature "{prerequisite}"'))
def flag_dependency(
state: ScenarioState, flagsmith: FlagsmithAdmin, feature: str, prerequisite: str
) -> None:
flagsmith.create_flag_dependency(require_environment(state), feature, prerequisite)


# Then: the runner checks the outcome


Expand Down
Loading