Skip to content

fix: add defensive cycle guard to prerequisite evaluation#1816

Merged
tanderson-ld merged 5 commits into
mainfrom
ta/SDK-2707-cycle-detection
Jul 22, 2026
Merged

fix: add defensive cycle guard to prerequisite evaluation#1816
tanderson-ld merged 5 commits into
mainfrom
ta/SDK-2707-cycle-detection

Conversation

@tanderson-ld

@tanderson-ld tanderson-ld commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds defensive cycle detection to the recursive prerequisite walk in LDClientImpl._variationInternal, bringing the JS client-side SDKs' behavior into line with the LaunchDarkly server SDK evaluators.

The fix

_variationInternal now threads a lazily-allocated Set<string> through the recursion carrying the flag keys on the current evaluation path. Before descending into a prerequisite, the walker checks whether that key is already on the path; if so, it skips that edge and continues with remaining prerequisites at the same level.

  • Lazy allocation: variation calls on prereq-less flags (the common case) allocate zero collections. The Set<string> is created only when the walker descends into a prerequisite for the first time in a call, then reused for the rest of the walk.
  • Mutable add / try/finally delete: no per-descent copy. ancestors.add(flagKey) before recursing, ancestors.delete(flagKey) in the finally — the invariant "the set contains exactly the current path" holds even under early exits.
  • Ancestor-set (current-path) semantics — this is deliberate. A prerequisite reached via multiple non-cyclic paths (a diamond A -> [B, C], B -> [D], C -> [D]) is not a cycle; each path should emit its own prerequisite event. A global visited-set would silently drop the second event; the ancestor-set pattern correctly emits D twice. There is a unit test guarding this.

The optional visited?: Set<string> parameter is trailing, so the four existing entry points into _variationInternal (variation, variationDetail, and the two typed-eval callsites) work unchanged — none of the public variation / variationDetail / boolVariation / intVariation / stringVariation / doubleVariation / jsonVariation methods need to change.

Caller-visible behavior on a cycle

The requested flag returns its cached value and reason unchanged. A client-side prerequisite cycle is not surfaced as MALFORMED_FLAG and does not fall back to the caller-provided default value. This differs from server-side behavior — the client already holds an authoritative pre-evaluated result from the server; only the ancillary event walk is affected by the cycle.

Files changed

  • packages/shared/sdk-client/src/LDClientImpl.ts — cycle guard in _variationInternal. Existing callers unchanged.
  • packages/shared/sdk-client/__tests__/LDClientImpl.events.test.ts — 5 new Jest tests grouped under a nested describe('prerequisite cycles', ...): self-loop, two-cycle (evaluating each end), three-cycle, and a non-cyclic diamond that asserts the shared descendant emits an event for each path (the ancestor-set regression fence).
  • packages/sdk/browser/contract-tests/entity/src/TestHarnessWebSocket.ts — declares client-prereq-cycle-detection so the matching contract tests in sdk-test-harness v2.38.0+ activate for the browser SDK. Downstream wrapper packages (React, React Native, Electron, node-client, Vue) will declare the capability in follow-up PRs against SDK-2711 through SDK-2715.

Verification

  • Unit tests: yarn workspace @launchdarkly/js-client-sdk-common run test → 834 tests, 0 failures. The 5 new cycle-detection tests are visible under the prerequisite cycles group and passing.
  • Contract tests locally against harness v2.38.1: full monorepo build, browser adapter + entity + headless Chromium, harness run → 831 total, 32 skipped, 799 ran, all passed. Both new suites (events/prerequisite events handle cycles and events/summary events/prerequisites/handles cycles) fire and pass — 15 subtests total between them.

Changelog

Updated prerequisite evaluation event emission to match server SDK behavior for cyclic prerequisite graphs.


Note

Medium Risk
Touches the core variation() prerequisite/event path, but behavior for non-cyclic flags is unchanged and cycles only skip extra recursion while preserving cached values.

Overview
Prerequisite evaluation in _variationInternal now tracks the current dependency path in a lazily allocated Set and skips cyclic edges instead of recursing forever. Returned flag values and reasons stay the same; only the prerequisite walk (and related feature events) is bounded, matching server SDK semantics. Diamond graphs still emit one prerequisite event per distinct path—not a single global “visited” pass.

Adds five unit tests for self-loops, 2/3-cycles, and a non-cyclic diamond regression. The browser contract harness advertises client-prereq-cycle-detection for harness v2.38+. CI bundle size limit for @launchdarkly/js-client-sdk-common rises slightly (39,000 → 39,300 bytes).

Reviewed by Cursor Bugbot for commit 2b03e72. Bugbot is set up for automated code reviews on this repo. Configure here.

Adds an ancestor-set (current-path) cycle guard to the recursive
prerequisite walk in _variationInternal, bringing the shared JS
client-side SDK's behavior into line with the LaunchDarkly server SDK
evaluators, which have detected and gracefully handled cyclic
prerequisite graphs for years.

The LaunchDarkly service validates prerequisite graphs on mutation and
rejects any change that would produce a cycle, so under normal
operation the SDK does not see a cyclic graph. This is defensive code
for exceptional cases -- for example, delivery of updates out of order
or a persisted state loaded from disk that predates a subsequent
correction.

The Set tracking ancestor keys is allocated lazily: variation calls on
prereq-less flags (the common case) allocate zero collections. Once
created, the set is shared for the rest of the walk via
add-before-recurse / delete-after-recurse, guarded by try/finally so a
recursive descent that throws cannot leave a stale ancestor entry
visible to a sibling branch.

When a cycle is detected the requested flag's cached value and reason
are returned unchanged; only the recursive prerequisite event walk is
affected.

Also declares the client-prereq-cycle-detection capability on the
browser SDK's contract-test service so the matching sdk-test-harness
contract tests activate for @launchdarkly/js-client-sdk. Downstream
wrappers (React, React Native, Electron, node-client, Vue) will
declare the capability as part of separate follow-up PRs.
@tanderson-ld
tanderson-ld requested a review from a team as a code owner July 21, 2026 17:28
@github-actions

Copy link
Copy Markdown
Contributor

@launchdarkly/js-sdk-common size report
This is the brotli compressed size of the ESM build.
Compressed size: 26360 bytes
Compressed size limit: 29000
Uncompressed size: 129188 bytes

@github-actions

Copy link
Copy Markdown
Contributor

@launchdarkly/browser size report
This is the brotli compressed size of the ESM build.
Compressed size: 179796 bytes
Compressed size limit: 200000
Uncompressed size: 831704 bytes

@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

@launchdarkly/js-client-sdk-common size report
This is the brotli compressed size of the ESM build.
Compressed size: 39108 bytes
Compressed size limit: 39300
Uncompressed size: 213642 bytes

@github-actions

Copy link
Copy Markdown
Contributor

@launchdarkly/js-client-sdk size report
This is the brotli compressed size of the ESM build.
Compressed size: 32077 bytes
Compressed size limit: 34000
Uncompressed size: 114525 bytes

Comment thread packages/shared/sdk-client/src/LDClientImpl.ts Outdated
'anonymous-redaction',
'strongly-typed',
'client-prereq-events',
'client-prereq-cycle-detection',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we try to enable this capability on react sdk as well (since it is just a wrapper)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, planning to in the story for React

tanderson-ld and others added 2 commits July 21, 2026 14:36
@tanderson-ld
tanderson-ld merged commit 9426b42 into main Jul 22, 2026
55 checks passed
@tanderson-ld
tanderson-ld deleted the ta/SDK-2707-cycle-detection branch July 22, 2026 13:23
@github-actions github-actions Bot mentioned this pull request Jul 22, 2026
tanderson-ld added a commit to launchdarkly/flutter-client-sdk that referenced this pull request Jul 22, 2026
## Summary

Adds defensive cycle detection to the recursive prerequisite walk in
`LDCommonClient._variationInternal`, bringing the Flutter client SDK's
behavior into line with the LaunchDarkly server SDK evaluators.

Tracker: [SDK-2705](https://launchdarkly.atlassian.net/browse/SDK-2705).
Parent: [SDK-2695](https://launchdarkly.atlassian.net/browse/SDK-2695).
Spec change:
[sdk-specs#246](launchdarkly/sdk-specs#246)
(CSPE 1.2.5, 1.2.5.1, 1.2.5.2). Contract-test coverage:
[sdk-test-harness#384](launchdarkly/sdk-test-harness#384)
(merged in v2.38.0). Companion PRs:
[android-client-sdk#377](launchdarkly/android-client-sdk#377),
[ios-client-sdk#512](launchdarkly/ios-client-sdk#512),
[js-core#1816](launchdarkly/js-core#1816).

## Background

The LaunchDarkly service validates prerequisite graphs on every mutation
and rejects any change that would produce a cycle, so under normal
operation the SDK does not see a cyclic prerequisite graph. Server-side
SDK evaluators nonetheless carry defensive cycle detection for
exceptional cases — for example, delivery of updates out of order or a
persisted state loaded from disk that predates a subsequent correction.
This PR extends the same defensive posture to the Flutter client SDK.

## The fix

`_variationInternal` now threads a lazily-allocated `Set<String>?`
through the recursion carrying the flag keys on the current evaluation
path. Before descending into a prerequisite, the walker checks whether
that key is already on the path; if so, it skips that edge and continues
with remaining prerequisites at the same level.

- **Lazy allocation**: variation calls on prereq-less flags (the common
case) allocate zero collections. The `Set<String>` is created only when
the walker descends into a prerequisite for the first time in a call,
then reused for the rest of the walk.
- **Mutable `add` / `finally remove`**: no per-descent copy.
`ancestors.add(flagKey)` before recursing, `ancestors.remove(flagKey)`
in a `finally` — the invariant "the set contains exactly the current
path" holds even under early exits.
- **Ancestor-set (current-path) semantics** — this is deliberate. A
prerequisite reached via multiple non-cyclic paths (a diamond `A -> [B,
C], B -> [D], C -> [D]`) is not a cycle; each path should emit its own
prerequisite event. A global visited-set would silently drop the second
event; the ancestor-set pattern correctly emits D twice. There is a unit
test guarding this.

The optional named parameter `visited` is added last, so all existing
callers (the seven typed and untyped `variation` / `variationDetail`
entry points) work unchanged.

## Caller-visible behavior on a cycle

The requested flag returns its cached value and reason unchanged. A
client-side prerequisite cycle is not surfaced as `MALFORMED_FLAG` and
does not fall back to the caller-provided default value. This differs
from server-side behavior — the client already holds an authoritative
pre-evaluated result from the server; only the ancillary event walk is
affected by the cycle.

## Files changed

- `packages/common_client/lib/src/ld_common_client.dart` — cycle guard
in `_variationInternal`.
- `packages/common_client/test/ld_dart_client_test.dart` — 5 new tests
under the existing `given mock flag data with prerequisites` group:
self-loop, two-cycle (evaluating each end), three-cycle, and a
non-cyclic diamond that asserts the shared descendant emits an event for
each path (the ancestor-set regression fence).
-
`apps/flutter_client_contract_test_service/bin/contract_test_service.dart`
— declares `client-prereq-cycle-detection` so the matching contract
tests in sdk-test-harness v2.38.0+ activate for this SDK.

## Verification

- **Unit tests**: `dart test test/ld_dart_client_test.dart` → 31 tests,
0 failures. The 5 new cycle-detection tests all pass.

## Changelog

> Updated prerequisite evaluation event emission to match server SDK
behavior for cyclic prerequisite graphs.

[SDK-2705]:
https://launchdarkly.atlassian.net/browse/SDK-2705?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
[SDK-2695]:
https://launchdarkly.atlassian.net/browse/SDK-2695?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes flag evaluation’s prerequisite recursion and analytics event
ordering, but behavior is narrowly scoped to abnormal cyclic graphs and
is covered by new unit and contract tests.
> 
> **Overview**
> Adds **defensive cycle detection** to the recursive prerequisite walk
in `LDCommonClient._variationInternal`, matching server SDK behavior
when a cyclic prereq graph appears (e.g. stale persisted state).
> 
> The walker threads a lazily allocated **ancestor set** (current path
only): before recursing into a prerequisite it skips edges whose key is
already on the path, then continues with other prerequisites. The
evaluated flag still returns its **cached value and reason**; only the
ancillary prereq event walk is affected. **Diamond graphs** still emit
one prereq event per independent path (not treated as cycles).
> 
> Adds five unit tests (self-loop, 2- and 3-cycles, diamond regression)
and advertises the **`client-prereq-cycle-detection`** contract-test
capability on the Flutter contract test service.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
7ea0310. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
tanderson-ld added a commit to launchdarkly/dotnet-core that referenced this pull request Jul 23, 2026
## Summary

Adds defensive cycle detection to the recursive prerequisite walk in
`LdClient.EvaluateInternal`, bringing the .NET client SDK's behavior
into line with the LaunchDarkly server SDK evaluators.

Tracker: [SDK-2708](https://launchdarkly.atlassian.net/browse/SDK-2708).
Parent: [SDK-2695](https://launchdarkly.atlassian.net/browse/SDK-2695).
Spec change:
[sdk-specs#246](launchdarkly/sdk-specs#246)
(CSPE 1.2.5, 1.2.5.1, 1.2.5.2). Contract-test coverage:
[sdk-test-harness#384](launchdarkly/sdk-test-harness#384)
(merged in v2.38.0). Companion PRs:
[android-client-sdk#377](launchdarkly/android-client-sdk#377),
[ios-client-sdk#512](launchdarkly/ios-client-sdk#512),
[js-core#1816](launchdarkly/js-core#1816),
[flutter-client-sdk#329](launchdarkly/flutter-client-sdk#329).

## Background

The LaunchDarkly service validates prerequisite graphs on every mutation
and rejects any change that would produce a cycle, so under normal
operation the SDK does not see a cyclic prerequisite graph. Server-side
SDK evaluators nonetheless carry defensive cycle detection for
exceptional cases — for example, delivery of updates out of order or a
persisted state loaded from disk that predates a subsequent correction.
Note that the .NET **server** SDK evaluator already carries this guard
(`PrereqFlagKeyStack` in `Evaluator.cs`); this PR extends the same
posture to the client SDK.

## The fix

`EvaluateInternal<T>` now threads a lazily-allocated `HashSet<string>`
through the recursion carrying the flag keys on the current evaluation
path. Before descending into a prerequisite, the walker checks whether
that key is already on the path; if so, it skips that edge and continues
with remaining prerequisites at the same level.

- **Lazy allocation**: variation calls on prereq-less flags (the common
case) allocate zero collections. The `HashSet<string>` is created only
when the walker descends into a prerequisite for the first time in a
call, then reused for the rest of the walk.
- **Mutable `Add` / `finally Remove`**: no per-descent copy.
`ancestors.Add(featureKey)` before recursing,
`ancestors.Remove(featureKey)` in a `finally` — the invariant "the set
contains exactly the current path" holds even under exceptions.
- **Ancestor-set (current-path) semantics** — this is deliberate. A
prerequisite reached via multiple non-cyclic paths (a diamond `A -> [B,
C], B -> [D], C -> [D]`) is not a cycle; each path should emit its own
prerequisite event. A global visited-set would silently drop the second
event; the ancestor-set pattern correctly emits D twice. There is a unit
test guarding this.

The optional trailing parameter `HashSet<string> visited = null` is
added to `EvaluateInternal<T>`, so all existing callers (the public
`BoolVariation` / `IntVariation` / `StringVariation` / `DoubleVariation`
/ `JsonVariation` and their `*Detail` counterparts) work unchanged.

## Caller-visible behavior on a cycle

The requested flag returns its cached value and reason unchanged. A
client-side prerequisite cycle is not surfaced as `MALFORMED_FLAG` and
does not fall back to the caller-provided default value. This differs
from server-side behavior — the client already holds an authoritative
pre-evaluated result from the server; only the ancillary event walk is
affected by the cycle.

## Files changed

- `pkgs/sdk/client/src/LdClient.cs` — cycle guard in
`EvaluateInternal<T>`.
-
`pkgs/sdk/client/test/LaunchDarkly.ClientSdk.Tests/LdClientEventTests.cs`
— 5 new xUnit tests: self-loop, two-cycle (evaluating each end),
three-cycle, and a non-cyclic diamond that asserts the shared descendant
emits an event for each path (the ancestor-set regression fence).
- `pkgs/sdk/client/contract-tests/TestService.cs` — declares
`client-prereq-cycle-detection` so the matching contract tests in
sdk-test-harness v2.38.0+ activate for this SDK.

## Verification

- **Unit tests**: `dotnet test -f net8.0 --filter
"Prerequisite|Cycle|Diamond"` → 6 passed, 0 failures (the original
prereq event test plus 5 new cycle-detection tests).

## Changelog

> Updated prerequisite evaluation event emission to match server SDK
behavior for cyclic prerequisite graphs.

[SDK-2708]:
https://launchdarkly.atlassian.net/browse/SDK-2708?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
[SDK-2695]:
https://launchdarkly.atlassian.net/browse/SDK-2695?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches core flag evaluation and analytics event emission for
prerequisite walks; behavior change is defensive but affects event
ordering/counts on malformed graphs.
> 
> **Overview**
> Adds **defensive cycle detection** to the recursive prerequisite walk
in `LdClient.EvaluateInternal`, aligning client SDK behavior with server
evaluators and sdk-spec CSPE 1.2.5.
> 
> Before descending into each prerequisite, the walker tracks flag keys
on the **current path** in a lazily allocated `HashSet<string>` (add
before recurse, remove in `finally`). If a prerequisite key is already
on that path, that edge is skipped; the requested flag still returns its
**cached value and reason** unchanged.
> 
> **Ancestor-set semantics** (not a global visited set) preserve correct
analytics for diamond graphs: a shared prerequisite reached via separate
branches still emits one evaluation event per path.
> 
> Contract tests declare capability `client-prereq-cycle-detection`.
Five xUnit tests cover self-loops, 2- and 3-cycles, and the diamond
regression case.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
81fbdce. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
joker23 pushed a commit that referenced this pull request Jul 23, 2026
🤖 I have created a release *beep* *boop*
---


<details><summary>browser: 0.1.30</summary>

##
[0.1.30](browser-v0.1.29...browser-v0.1.30)
(2026-07-22)


### Dependencies

* The following workspace dependencies were updated
  * dependencies
    * @launchdarkly/js-client-sdk bumped from 4.9.2 to 4.9.3
</details>

<details><summary>client-testing-plugin: 1.0.6</summary>

##
[1.0.6](client-testing-plugin-v1.0.5...client-testing-plugin-v1.0.6)
(2026-07-22)


### Dependencies

* The following workspace dependencies were updated
  * dependencies
    * @launchdarkly/js-client-sdk-common bumped from 1.30.0 to 1.30.1
  * devDependencies
    * @launchdarkly/js-client-sdk bumped from 4.9.2 to 4.9.3
    * @launchdarkly/react-sdk bumped from 4.1.7 to 4.1.8
  * peerDependencies
    * @launchdarkly/js-client-sdk bumped from ^4.9.1 to ^4.9.3
    * @launchdarkly/react-sdk bumped from ^4.1.4 to ^4.1.8
</details>

<details><summary>jest: 1.0.25</summary>

##
[1.0.25](jest-v1.0.24...jest-v1.0.25)
(2026-07-22)


### Dependencies

* The following workspace dependencies were updated
  * dependencies
* @launchdarkly/react-native-client-sdk bumped from ~10.19.2 to ~10.19.3
</details>

<details><summary>js-client-sdk: 4.9.3</summary>

##
[4.9.3](js-client-sdk-v4.9.2...js-client-sdk-v4.9.3)
(2026-07-22)


### Bug Fixes

* add defensive cycle guard to prerequisite evaluation
([#1816](#1816))
([9426b42](9426b42))


### Dependencies

* The following workspace dependencies were updated
  * dependencies
    * @launchdarkly/js-client-sdk-common bumped from 1.30.0 to 1.30.1
</details>

<details><summary>js-client-sdk-common: 1.30.1</summary>

##
[1.30.1](js-client-sdk-common-v1.30.0...js-client-sdk-common-v1.30.1)
(2026-07-22)


### Bug Fixes

* add defensive cycle guard to prerequisite evaluation
([#1816](#1816))
([9426b42](9426b42))
</details>

<details><summary>node-client-sdk: 0.4.3</summary>

##
[0.4.3](node-client-sdk-v0.4.2...node-client-sdk-v0.4.3)
(2026-07-22)


### Dependencies

* The following workspace dependencies were updated
  * dependencies
    * @launchdarkly/js-client-sdk-common bumped from 1.30.0 to 1.30.1
</details>

<details><summary>react-native-client-sdk: 10.19.3</summary>

##
[10.19.3](react-native-client-sdk-v10.19.2...react-native-client-sdk-v10.19.3)
(2026-07-22)


### Dependencies

* The following workspace dependencies were updated
  * dependencies
    * @launchdarkly/js-client-sdk-common bumped from 1.30.0 to 1.30.1
</details>

<details><summary>react-sdk: 4.1.8</summary>

##
[4.1.8](react-sdk-v4.1.7...react-sdk-v4.1.8)
(2026-07-22)


### Dependencies

* The following workspace dependencies were updated
  * dependencies
    * @launchdarkly/js-client-sdk bumped from ^4.9.2 to ^4.9.3
</details>

<details><summary>server-sdk-ai: 1.3.0</summary>

##
[1.3.0](server-sdk-ai-v1.2.3...server-sdk-ai-v1.3.0)
(2026-07-22)


### Features

* **server-ai:** stamp modelKey and modelVersion on AI usage events
(AIC-2858)
([#1794](#1794))
([a91a1e7](a91a1e7))
</details>

<details><summary>server-sdk-ai-langchain: 0.8.8</summary>

##
[0.8.8](server-sdk-ai-langchain-v0.8.7...server-sdk-ai-langchain-v0.8.8)
(2026-07-22)


### Dependencies

* The following workspace dependencies were updated
  * devDependencies
    * @launchdarkly/server-sdk-ai bumped from ^1.2.3 to ^1.3.0
  * peerDependencies
    * @launchdarkly/server-sdk-ai bumped from ^1.1.1 to ^1.3.0
</details>

<details><summary>server-sdk-ai-openai: 0.7.8</summary>

##
[0.7.8](server-sdk-ai-openai-v0.7.7...server-sdk-ai-openai-v0.7.8)
(2026-07-22)


### Dependencies

* The following workspace dependencies were updated
  * devDependencies
    * @launchdarkly/server-sdk-ai bumped from ^1.2.3 to ^1.3.0
  * peerDependencies
    * @launchdarkly/server-sdk-ai bumped from ^1.1.1 to ^1.3.0
</details>

<details><summary>server-sdk-ai-vercel: 0.7.8</summary>

##
[0.7.8](server-sdk-ai-vercel-v0.7.7...server-sdk-ai-vercel-v0.7.8)
(2026-07-22)


### Dependencies

* The following workspace dependencies were updated
  * devDependencies
    * @launchdarkly/server-sdk-ai bumped from ^1.2.3 to ^1.3.0
  * peerDependencies
    * @launchdarkly/server-sdk-ai bumped from ^1.1.1 to ^1.3.0
</details>

<details><summary>vue-client-sdk: 0.1.3</summary>

##
[0.1.3](vue-client-sdk-v0.1.2...vue-client-sdk-v0.1.3)
(2026-07-22)


### Dependencies

* The following workspace dependencies were updated
  * dependencies
    * @launchdarkly/js-client-sdk bumped to 4.9.3
</details>

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> The release carries a flag-evaluation behavior change (cycle handling)
and AI telemetry shape changes; risk is moderate for apps with cyclic
prerequisites or AI usage analytics, though the diff here is mostly
versioning.
> 
> **Overview**
> This is a **Release Please** cut that bumps versions across the
monorepo and refreshes changelogs, `.release-please-manifest.json`,
workspace dependency pins, and embedded SDK version strings—no new
application logic in the diff itself.
> 
> **`@launchdarkly/js-client-sdk-common` 1.30.1** (pulled into browser,
node-client, React Native, and related wrappers) documents a **defensive
cycle guard during prerequisite flag evaluation**, avoiding unbounded
recursion when prerequisite graphs contain cycles
([#1816](#1816)).
> 
> **`@launchdarkly/server-sdk-ai` 1.3.0** adds **`modelKey` and
`modelVersion` on AI usage / tracker events** from flag `_ldMeta`
([#1794](#1794)).
LangChain, OpenAI, and Vercel AI provider packages are re-released with
updated `server-sdk-ai` peer/dev dependencies only.
> 
> Downstream packages (React, Vue, combined browser,
client-testing-plugin, jest tooling, examples) are version-aligned to
pick up the client-common fix transitively where applicable.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
35513d5. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
tanderson-ld added a commit to launchdarkly/roku-client-sdk that referenced this pull request Jul 23, 2026
## Summary

Adds defensive cycle detection to the recursive prerequisite walk in
`variationDetail` (`LaunchDarklyClient.brs`), bringing the Roku client
SDK's behavior into line with the LaunchDarkly server SDK evaluators.

Tracker: [SDK-2710](https://launchdarkly.atlassian.net/browse/SDK-2710).
Parent: [SDK-2695](https://launchdarkly.atlassian.net/browse/SDK-2695).
Spec change:
[sdk-specs#246](launchdarkly/sdk-specs#246)
(CSPE 1.2.5, 1.2.5.1, 1.2.5.2). Contract-test coverage:
[sdk-test-harness#384](launchdarkly/sdk-test-harness#384)
(merged in v2.38.0). Companion PRs:
[android-client-sdk#377](launchdarkly/android-client-sdk#377),
[ios-client-sdk#512](launchdarkly/ios-client-sdk#512),
[js-core#1816](launchdarkly/js-core#1816),
[flutter-client-sdk#329](launchdarkly/flutter-client-sdk#329),
[dotnet-core#317](launchdarkly/dotnet-core#317),
[cpp-sdks#582](launchdarkly/cpp-sdks#582).

## Background

The LaunchDarkly service validates prerequisite graphs on every mutation
and rejects any change that would produce a cycle, so under normal
operation the SDK does not see a cyclic prerequisite graph. Server-side
SDK evaluators nonetheless carry defensive cycle detection for
exceptional cases — for example, delivery of updates out of order or a
persisted state loaded from disk that predates a subsequent correction.
This PR extends the same defensive posture to the Roku client SDK.

## The fix

`variationDetail` now threads a lazily-allocated associative array
through the recursion carrying the flag keys on the current evaluation
path. Before descending into a prerequisite, the walker checks whether
that key is already on the path; if so, it skips that edge and continues
with remaining prerequisites at the same level.

- **Lazy allocation**: variation calls on prereq-less flags (the common
case) allocate no associative array. The map is created only when the
walker descends into a prerequisite for the first time in a call, then
reused for the rest of the walk.
- **Mutable add/delete around the loop**: no per-descent copy. The
current flag's key is inserted before the loop and removed after, so the
invariant "the map contains exactly the current path" holds across
sibling descents.
- **Ancestor-set (current-path) semantics** — this is deliberate. A
prerequisite reached via multiple non-cyclic paths (a diamond `A -> [B,
C], B -> [D], C -> [D]`) is not a cycle; each path should emit its own
prerequisite event. A global visited-set would silently drop the second
event; the ancestor-set pattern correctly emits D twice. There is a unit
test guarding this.

The optional trailing parameter `launchDarklyParamVisited=invalid` is
added to `variationDetail`, so existing callers (`boolVariation`,
`intVariationDetail`, `stringVariationDetail`, `doubleVariationDetail`,
`jsonVariationDetail`, and the untyped `variation`) work unchanged.

## Caller-visible behavior on a cycle

The requested flag returns its cached value and reason unchanged. A
client-side prerequisite cycle is not surfaced as `MALFORMED_FLAG` and
does not fall back to the caller-provided default value. This differs
from server-side behavior — the client already holds an authoritative
pre-evaluated result from the server; only the ancillary event walk is
affected by the cycle.

## Files changed

- `rawsrc/LaunchDarklyClient.brs` — cycle guard in `variationDetail`.
- `src/test/source/tests/Test__Client.brs` — 5 new unit tests:
self-loop, two-cycle (evaluating each end), three-cycle, and a
non-cyclic diamond that asserts the shared descendant emits an event for
each path (the ancestor-set regression fence).
- `src/contract-tests/components/HttpServerTask.brs` — declares
`client-prereq-cycle-detection` so the matching contract tests in
sdk-test-harness v2.38.0+ activate for this SDK.

## Verification

- **Unit tests**: 5 new `TestCase__Client_CycleDetection_*` cases follow
the existing `TestCase__Client_*` pattern (seed `client.private.store`,
call `client.variation()`, flush and inspect the event queue). Local
execution requires a Roku device (`ukor test`); CI provides the actual
test run.
- **Contract tests**: local run requires deployment to a Roku device; CI
provides the run against the released harness. The new suite
(`client-prereq-cycle-detection`) will activate as soon as CI picks up
harness v2.38.0+.

## Changelog

> Updated prerequisite evaluation event emission to match server SDK
behavior for cyclic prerequisite graphs.

[SDK-2710]:
https://launchdarkly.atlassian.net/browse/SDK-2710?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
[SDK-2695]:
https://launchdarkly.atlassian.net/browse/SDK-2695?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches the prerequisite event-walk in core flag evaluation, but
behavior for normal graphs is preserved and cycles only skip redundant
descent; coverage is strong via new unit and contract capability tests.
> 
> **Overview**
> Adds **defensive cycle detection** when the Roku client walks flag
**prerequisites** to emit evaluation events, matching other LaunchDarkly
SDKs and avoiding unbounded recursion on malformed graphs.
> 
> Core `variationDetail` logic moves into
**`LaunchDarklyClientSharedPrivateFunctions`** and is merged into both
the standard client and SceneGraph client. The recursive walk threads a
**current-path ancestor set** (`launchDarklyParamVisited`): before
descending into a prerequisite, if that flag is already on the path the
edge is skipped; diamonds still emit events per path (not a global
visited set). **Public variation APIs are unchanged**—they delegate to
the private implementation with `m.status`.
> 
> Contract tests advertise capability
**`client-prereq-cycle-detection`**. **Five unit tests** cover
self-loop, 2- and 3-cycles, and a diamond graph (cached values
unchanged; feature event order asserted).
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
1e59528. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants