Skip to content

feat: Split summary counters by overrideAffected and suppress individual events for marked evaluations - #365

Merged
kinyoklion merged 14 commits into
dotnet-server-sdk-v9from
rlamb/overrides-dotnet-events-internal
Oct 29, 2026
Merged

kinyoklion merged 14 commits into
dotnet-server-sdk-v9from
rlamb/overrides-dotnet-events-internal

Conversation

@kinyoklion

@kinyoklion kinyoklion commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

Summary

Extends the analytics event processing in LaunchDarkly.InternalSdk for override-affected evaluations, as defined by the OVERRIDE specification.

EventTypes.EvaluationEvent gains an OverrideAffected field. When it is set, the event processor produces no individual feature event and no debug event for the evaluation, whatever the flag's TrackEvents and DebugEventsUntilDate request. The evaluation is still counted in summary events, and the marker becomes part of the summary counter key, so override-affected and other evaluations of the same flag key, variation, and version accumulate into separate counters instead of being collapsed together. The summary event output writes "overrideAffected": true on a marked counter and omits the property otherwise, following the existing unknown marker. The summarizer interface carries the marker through both the aggregated and the per-context summarizers.

Nothing sets the field yet; the server SDK does so in the next change. Every existing event path is unchanged when the field is false, which the existing event processor and output tests confirm. New tests cover the suppression of feature and debug events, the suppression of a marked prerequisite record, the separate counters, and the counter serialization.

This PR is based on the branch of the override layer change so that the whole flag overrides series applies as one linear sequence; it uses nothing from that branch.

SDK-3248


Note

Overview
Adds an experimental flag/segment override layer to the FDv2 data system so operators can force known flag state per key without affecting initialization or data-source status.

Configuration and plumbing: DataSystemBuilder.Overrides() wires an optional IOverrideSource into DataSystemConfiguration. FDv2 builds an OverrideLayer, OverrideSink, and OverrideOverlay on the store read path; FDv1 exposes empty/no-op override APIs. Offline mode skips starting the source (warn only). Start/dispose failures are logged and the client continues without overrides when appropriate.

Evaluation and client behavior: Overridden entries win on read; flags can be evaluated (and AllFlagsState can return override-only payloads) before LaunchDarkly data is available when the layer holds them. Evaluation reasons carry overrideAffected through errors and migration paths; AllFlagsState disables per-flag tracking fields for override-affected results.

Change notifications: LaunchDarkly updates fan out through combined dependency tracking (IExternalDependencies on the override layer), and override snapshots notify listeners via the same flag-change path with merged-view diffing in OverrideChanges.

Analytics: EvaluationEvent.OverrideAffected suppresses individual feature and debug events while still counting summaries; counters are keyed separately and serialize "overrideAffected": true when set.

Extensive unit/integration tests cover the layer, overlay, sink, builder, client scenarios, OVERRIDE spec vectors, and event summarizer/processor output.

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

@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-override-layer branch from 49ba8e2 to d9e0211 Compare September 28, 2026 20:39
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-events-internal branch from 9387b50 to 3aacd97 Compare September 28, 2026 20:39
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-override-layer branch from d9e0211 to 092ed5c Compare October 1, 2026 23:43
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-events-internal branch from 3aacd97 to 471a60c Compare October 1, 2026 23:43
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-override-layer branch from 092ed5c to 716b005 Compare October 3, 2026 01:34
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-events-internal branch from 471a60c to a750428 Compare October 3, 2026 01:34
@kinyoklion

Copy link
Copy Markdown
Member Author

bugbot review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit a750428. Configure here.

@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-override-layer branch from 716b005 to 041e7a5 Compare October 3, 2026 01:52
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-events-internal branch from a750428 to c9ce308 Compare October 3, 2026 01:52
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-override-layer branch from 041e7a5 to 91ecadb Compare October 5, 2026 21:55
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-events-internal branch from c9ce308 to 18760a7 Compare October 5, 2026 21:55
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-override-layer branch from 91ecadb to 5d664c1 Compare October 5, 2026 22:12
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-events-internal branch from 18760a7 to 3941b93 Compare October 5, 2026 22:12
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-override-layer branch from 5d664c1 to c875758 Compare October 5, 2026 22:28
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-events-internal branch from 3941b93 to 17cf04b Compare October 5, 2026 22:28
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-override-layer branch from c875758 to 8ad94ca Compare October 5, 2026 23:56
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-events-internal branch from 17cf04b to fb8c7ac Compare October 5, 2026 23:56
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-override-layer branch from 8ad94ca to b47e5ff Compare October 13, 2026 21:03
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-events-internal branch from fb8c7ac to e0080d0 Compare October 13, 2026 21:03
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-override-layer branch from b47e5ff to 0864b85 Compare October 13, 2026 22:13
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-events-internal branch 2 times, most recently from 3469298 to 1531cd9 Compare October 16, 2026 21:21
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-override-layer branch from 625311a to 0d26639 Compare October 23, 2026 23:50
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-events-internal branch from 1531cd9 to ac67f94 Compare October 23, 2026 23:50
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-override-layer branch from 0d26639 to c1ce57c Compare October 26, 2026 22:46
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-events-internal branch from ac67f94 to e4a339c Compare October 26, 2026 22:46
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-override-layer branch from c1ce57c to c93aba9 Compare October 26, 2026 23:13
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-events-internal branch from e4a339c to e6beb28 Compare October 26, 2026 23:13
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-override-layer branch from c93aba9 to ba14e56 Compare October 27, 2026 20:23
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-events-internal branch from 0da3d0f to 917b778 Compare October 28, 2026 21:04
…stem wiring

Adds IOverrideSource and IOverrideSink, the DataSystemBuilder.Overrides
option, and the override layer: an immutable store of marked entries, an
overlay at the store read boundary that returns the override entry for a
key in preference to LaunchDarkly data, and a sink that applies snapshots
one at a time and sends flag change notifications for the flags an update
may have changed, with dependency fan-out through prerequisites and
segments.

The FDv2 data system builds the configured source, starts it before the
data sources so its initial load completes inside the client constructor,
and disposes it with the client. The client consults the override store
before its not-initialized short-circuit, so an overridden flag is served
before LaunchDarkly data arrives. AllFlagsState reads through the overlay
and presents override-affected flags with event tracking off. The OVERRIDE
specification's test vectors run as a unit test.
A definition whose dependencies cannot be computed, such as a prerequisite
without a key or a segment match on a value that is not a string, is logged
and contributes no dependency edges, so every directly changed key is still
notified. A failure to determine or deliver the notifications is logged
instead of reaching the override source.
…uit and dispose on a failed start

Before the client is initialized, an evaluation proceeds only when the
override layer holds the flag; otherwise it takes the same path as without
an override source. The exception paths of evaluation and of the all-flags
state keep the override indicator when the definition came from the layer.
An override source whose Build or Start throws fails the client constructor
after the components created so far are disposed.
…n changes

A flag override can reference LaunchDarkly data: a rule that matches a LaunchDarkly segment, or a
prerequisite that is a LaunchDarkly flag. DataSourceUpdatesImpl computes flag change notifications
from a dependency tracker built from LaunchDarkly data alone, so when such a segment or flag
changed, the flags whose overrides depend on it were not notified. The override layer now keeps the
dependency edges of its own entries, rebuilt on every replacement, and the data source updates ask
it for the override entries that depend on a changed item. The chain can cross in both directions,
so each newly reached item is expanded through both sets of edges until nothing new is found. An
entry whose references cannot be read contributes no edges, the same as the change computation
skips it.
An exception from the override source's Start no longer fails the client constructor. The data
system logs it, disposes the source, and the client runs as it does without a source: an
application in an incident is better served by a client that runs without its overrides than by
one that does not start. The SDK's own file-based source never throws from Start, since it logs a
failed load and retries; a custom source could. The interface documents the behavior.
A source can supply a snapshot through the sink and then throw from Start. The data system now
closes the sink after disposing the failed source: the sink clears the layer, notifying the
flags that were briefly overridden, and ignores a late snapshot from the disposed source. The
client then runs without overrides, as the log says, and its all-flags state agrees.
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-override-layer branch from 4c1e37c to 87c85e3 Compare October 28, 2026 21:23
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-events-internal branch from 917b778 to 96a4008 Compare October 28, 2026 21:23
…source down safely

AllFlagsState returns a state of overrides alone only when the client is
not initialized, the store is not initialized, and the override layer
holds flags, and it reads those flags from the layer itself. Before, a
configured source was enough: an empty layer over a store that held
LaunchDarkly data without an init marker produced a valid state of that
data, and with overrides present the same data entered the state. The
overlay no longer serves the layer's entries alone when the base store
fails to list its items, so an initialized client whose store fails
reports an invalid state and logs the failure, as it does without a
source.

Disposing the client closes the override sink before it disposes the
source, so a snapshot supplied during shutdown is ignored, and an
exception from the source's Dispose is logged instead of stopping the
rest of the shutdown.

MigrationVariation keeps the override-affected marking when it replaces
the reason of an unrecognized stage with WRONG_TYPE.
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-events-internal branch from 96a4008 to 521a20b Compare October 28, 2026 21:53
An offline client does not build or start the override source, so its
overrides are not served and a configuration error in it is not
reported. The SDK now logs a warning when that happens, and the
Overrides configuration method documents it.
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-events-internal branch from 521a20b to f7eafb9 Compare October 28, 2026 22:02
A source logs and retries a failed load, and reports a configuration
error from Build. If Start throws anyway, the SDK logs it, disposes the
source, removes its snapshot, and the client runs without overrides.
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-events-internal branch from f7eafb9 to 52b82ae Compare October 28, 2026 22:35
… store fails

When the store cannot list its flags, the state without overrides is
invalid and empty. Overrides only add to it, so the state now holds the
override layer's flags and stays invalid, and the failure is still
logged. A client-side SDK bootstrapped from the state uses its flags
whatever its validity says, so the overridden values reach it. With no
override flags the state is invalid and empty, as without a source.
…ual events for marked evaluations

EventTypes.EvaluationEvent gains an OverrideAffected field. A marked
evaluation produces no individual feature event and no debug event,
whatever the flag requests, and is counted under a separate summary
counter whose key includes the marker. The summary output writes
"overrideAffected": true on such a counter and omits the property
otherwise, as defined by the OVERRIDE specification.
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-events-internal branch from 52b82ae to 56e48ec Compare October 29, 2026 16:45
Base automatically changed from rlamb/overrides-dotnet-override-layer to dotnet-server-sdk-v9 October 29, 2026 20:11
…e and debug paths

The prerequisite and debug tests checked only that a summary was sent.
They now check that its counter carries overrideAffected, so a marker
lost on either path fails them. The aggregated and per-context
summarizer tests gain a marked case, so a wrapper that dropped the new
argument fails them too.
@kinyoklion
kinyoklion merged commit c9b327b into dotnet-server-sdk-v9 Oct 29, 2026
10 checks passed
@kinyoklion
kinyoklion deleted the rlamb/overrides-dotnet-events-internal branch October 29, 2026 20:36
kinyoklion pushed a commit that referenced this pull request Oct 29, 2026
…alpha (#385)

🤖 I have created a release *beep* *boop*
---


##
[3.12.0-alpha](LaunchDarkly.InternalSdk-v3.11.1...LaunchDarkly.InternalSdk-v3.12.0-alpha)
(2026-10-29)


### Features

* Split summary counters by overrideAffected and suppress individual
events for marked evaluations
([#365](#365))
([c9b327b](c9b327b))

---
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]
> **Overview**
> Release Please PR that cuts **LaunchDarkly.InternalSdk
`3.12.0-alpha`**, updating the release manifest, package `Version`,
provenance docs, and changelog.
> 
> The release notes the **3.12.0** feature from
[#365](#365):
analytics **summary counters are keyed by `overrideAffected`**, and
evaluations with that marker **skip individual feature/debug events**
while still contributing to summaries.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
5f83938. 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>
kinyoklion added a commit that referenced this pull request Oct 29, 2026
<!-- TERMAG_PR_BODY_BEGIN -->
## Summary

Makes the server SDK hand the override-affected marking to the event
processor, as defined by the OVERRIDE specification.
`EventProcessorTypes.EvaluationEvent` gains an `OverrideAffected`
property, `EventFactory` sets it from the evaluation result for every
evaluation event and every prerequisite record, and
`DefaultEventProcessorWrapper` passes it to the InternalSdk event
processor, which then suppresses individual feature and debug events for
the marked evaluation and counts it under a separate summary counter
that carries the marker. The marking is carried whether or not the
caller asked for reasons.

A wrong-type result keeps the marking: the replacement error reason and
the event for that evaluation both report it, because the evaluation
read the same definitions. The specification's test vectors now also
assert the per-evaluation summary contribution, and new client tests
cover the marked evaluation record, the prerequisite tree in which only
one leaf is overridden, and the event output of a full flush, in which
only the unaffected sibling prerequisite produces a feature event while
all four flags have tracking on.

It depends on the InternalSdk events change, #365, released as
`LaunchDarkly.InternalSdk` 3.12.0-alpha, which the server now
references.

[SDK-3248](https://launchdarkly.atlassian.net/browse/SDK-3248)


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

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Overview**
> The server SDK now passes **override-affected** marking from
evaluations into analytics: `EvaluationEvent` gains
**`OverrideAffected`**, **`EventFactory`** sets it from the evaluation
reason (including prerequisite and wrong-type default-value events), and
**`DefaultEventProcessorWrapper`** forwards it to InternalSdk so marked
evaluations skip individual feature/debug events and appear in summary
counters with the marker—even when the caller did not request reasons.
> 
> The package reference bumps to **`LaunchDarkly.InternalSdk`
3.12.0-alpha** (depends on the InternalSdk events change). **Override
vector tests** assert `summaryOverrideAffected`, and new
**`LdClientOverrideEventsTest`** coverage exercises marking,
prerequisite trees, wrong-type paths, flush output, and the default
event processor wrapper.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
2b8016e. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

<!-- TERMAG_PR_BODY_END -->

<!-- termag: box=i-0575d1c54853058d7 project=projects -->
<sub>Opened from termag</sub>
kinyoklion pushed a commit that referenced this pull request Oct 29, 2026
…ha.1 (#382)

🤖 I have created a release *beep* *boop*
---


##
[9.0.0-alpha.1](LaunchDarkly.ServerSdk-v9.0.0-alpha.0...LaunchDarkly.ServerSdk-v9.0.0-alpha.1)
(2026-10-29)


### Features

* Add a shared file data reloader, poller, and watcher
([#361](#361))
([4eed241](4eed241))
* Add the file-based override source
([#367](#367))
([b834131](b834131))
* Add the override marker to the flag and segment models and mark
evaluations
([#363](#363))
([21f666a](21f666a))
* Add the override store, overlay, source interfaces, and data system
wiring ([#364](#364))
([e1748a1](e1748a1))
* Mark evaluation events as override-affected
([#366](#366))
([7c873a0](7c873a0))
* Split summary counters by overrideAffected and suppress individual
events for marked evaluations
([#365](#365))
([c9b327b](c9b327b))
* Support the flag-overrides capability in the contract test service
([#368](#368))
([b80d6aa](b80d6aa))

---
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]
> **Overview**
> This is a **Release Please** cut that publishes
**LaunchDarkly.ServerSdk `9.0.0-alpha.1`**. The diff only updates
release metadata: `.release-please-manifest.json`,
`LaunchDarkly.ServerSdk.csproj` `<Version>`, `PROVENANCE.md`
verification example, and a new **`9.0.0-alpha.1`** section in
`CHANGELOG.md`.
> 
> The changelog entry documents what ships in this alpha (already merged
elsewhere): **local flag overrides** via override store/overlay/source
wiring and a **file-based override source** with shared file
reloader/poller/watcher; **override markers** on flag/segment models and
evaluations; **analytics** changes that mark override-affected
evaluations, split summary counters by `overrideAffected`, and suppress
individual events when appropriate; plus **contract-test** support for
the flag-overrides capability.
> 
> Reviewers can treat this as a **versioning and release-notes sanity
check**—no application logic changes appear in the diff itself.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
81f0588. 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>
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