Skip to content

[FFL-1720] Evaluation Logging: Aggregation Engine & Test Utilities#3145

Merged
gh-worker-dd-mergequeue-cf854d[bot] merged 49 commits into
feature/flags-evaluations-loggingfrom
typo/FFL-1720-pr2-aggregation
Feb 13, 2026
Merged

[FFL-1720] Evaluation Logging: Aggregation Engine & Test Utilities#3145
gh-worker-dd-mergequeue-cf854d[bot] merged 49 commits into
feature/flags-evaluations-loggingfrom
typo/FFL-1720-pr2-aggregation

Conversation

@typotter

@typotter typotter commented Jan 22, 2026

Copy link
Copy Markdown
Contributor

🥞 Evaluation Logging Stacked Pull Requests 🥞

🔲 Integration & Configuration (PR #3147)
🔲 Storage & Network Infrastructure (PR #3146)
👉 Aggregation Engine & Test Utilities (this PR)
☑️ FlagEvaluation Schema (PR #3166)
☑️ Event Schema & Data Models (PR #3144)
☑️ Evaluations Subfeature (PR #3159)
feature/flags-evaluations-logging (feature branch)

Datadog Internal
🎟️ Ticket: FFL-1720 - Implement Evaluation Logging for Android SDK

What does this PR do?

Implements the core evaluations aggregation engine that groups feature flag evaluations by key characteristics and manages periodic flushing from memory to persistent storage (via StorageBackedFeature/EvaluationsFeature).

The EvaluationEventProcessor manages concurrent access and mutation of AggregationStats instances in memory. Each Stats object is given an AggregationKey - a composite key made of the set of identifiers across the evaluation (targetingKey, variationKey, etc.) and SDK context (rum View ID). This ensures proper evaluation counting under the "same" circumstances.

The processor also manages the periodic and size-based triggers for flushing the in-memory aggregations to persistent storage.

Motivation

We need to implement Evaluation Logging to provide comprehensive visibility into all feature flag evaluations, including defaults, errors, and successful matches. This goes beyond exposure logging by capturing aggregated metrics about evaluation frequency, error rates, and runtime default usage across all flags.

Description

This PR adds the core business logic for evaluation logging:

Implementation:

  • AggregationKey: Defines how evaluations are grouped (by flag, variant, allocation, targeting key, and error code)
  • AggregationStats: Tracks count, first/last timestamps, and last error message for each aggregation
  • EvaluationEventsProcessor: Orchestrates aggregation, manages flush triggers (time-based, size-based, shutdown)
  • EvaluationEventWriter: Interface for persisting evaluation events (implementation in PR [FFL-1720] Evaluation Logging: Storage & Network Infrastructure #3146)

Additional Notes

Review checklist (to be filled by reviewers)

  • Feature or bugfix MUST have appropriate tests (unit, integration, e2e)
  • Make sure you discussed the feature or bugfix with the maintaining team in an Issue
  • Make sure each commit and the PR mention the Issue number (cf the CONTRIBUTING doc)

@typotter
typotter force-pushed the typo/FFL-1720-pr2-aggregation branch from e1704c3 to 95f4c0a Compare January 22, 2026 09:27

typotter commented Jan 22, 2026

Copy link
Copy Markdown
Contributor Author

@datadog-official

This comment has been minimized.

Comment on lines +77 to +87
// Take atomic snapshot of statistics to prevent torn reads
val snapshotCount: Int
val snapshotFirst: Long
val snapshotLast: Long
val snapshotMessage: String?
synchronized(this) {
snapshotCount = count
snapshotFirst = firstEvaluation
snapshotLast = lastEvaluation
snapshotMessage = lastErrorMessage
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

minor: this piece is confusing. These fields are volatile, so torn read is not possible. Otherwise, this class is not designed to handle concurrent calls to recordEvaluation and toEvaluationEvent as that may leave some events uncounted.

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.

perhaps this is being overly defensive. Because of the CAS of the map of aggregation stats for flushing, _no other thread should be calling toEvaluationEvent or recordEvaluation on this AggregationStats instance.

Without this synchronized, recordEvaluation could change the computed values while we're getting them for constructing the FlagEvaluation

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.

With the synchronized CAS snapshotting the aggregationMap for flushing, this method should be protected from another thread calling recordEvaluation which could change the values as they're being grabbed for the FlagEvaluation constructor
Is this too defensive?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I drafted a small proposal to reduce the number of concurrency primitives usage 33544d7#r177031080 by making a trade-off for the immutability (which is more Kotlin-idiomatic). Feel free to adopt it or to reject it, whatever works for you. Performance penalty is negligible, but in exchange we reduce the number of code sites where we can run into concurrency bugs (which are hard to debug).

There we leverage event metadata to do the lightweight deserialization before the upload to drop events we are not interested in.

As of aggregation logic itself: we have a bit similar place in the SDK, where we reduce views, but there is it is in the boundaries of a single batch file and also the same model is used as a result (while here models are different).

@0xnm 0xnm Feb 12, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@dd-oleksii Cannot answer your comments on the commit directly, because new Github UI doesn't show them to me.

🐛 it's possible that timestamp is less than lastEvaluation, in which case lastEvaluation should not be updated. Similarly for firstEvaluation — it's possible that timestamp < firstEvlauation

Yes, it is a draft commit, I didn't 100% port this logic, but it is easy to fix with min / max functions.

🐛 Splitting size check and drain into different lock sections introduces a race condition and allows draining multiple times when size limit is exceeded (the number of drains is not limited to two).

Yes, a small sacrifice to have lighter locking. Worst case we just drain preliminary. Otherwise can be addressed by doing the size check again in the drain function inside the lock.

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.

Thanks @0xnm .
I have applied the proposed change, fixed the timestamp bug and shifted the locking to allow a second check of the map size within the lock in order to limit to 1 drain. PTAL

@typotter typotter mentioned this pull request Jan 27, 2026
3 tasks
@typotter
typotter force-pushed the typo/FFL-1720-pr1-schema-models branch 2 times, most recently from eb13adb to c924067 Compare January 27, 2026 19:25
@typotter
typotter force-pushed the typo/FFL-1720-pr2-aggregation branch from 97e4a4e to eb0ace5 Compare January 27, 2026 19:29
@codecov-commenter

codecov-commenter commented Jan 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.47887% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 70.97%. Comparing base (986afe7) to head (6858d57).

Files with missing lines Patch % Lines
...ndroid/flags/internal/EvaluationEventsProcessor.kt 95.16% 2 Missing and 1 partial ⚠️
...flags/internal/aggregation/EvaluationAggregator.kt 97.44% 0 Missing and 1 partial ⚠️
...id/flags/internal/net/EvaluationsRequestFactory.kt 0.00% 1 Missing ⚠️
Additional details and impacted files
@@                          Coverage Diff                          @@
##           feature/flags-evaluations-logging    #3145      +/-   ##
=====================================================================
+ Coverage                              70.81%   70.97%   +0.16%     
=====================================================================
  Files                                    901      905       +4     
  Lines                                  33172    33310     +138     
  Branches                                5596     5614      +18     
=====================================================================
+ Hits                                   23489    23641     +152     
+ Misses                                  8118     8102      -16     
- Partials                                1565     1567       +2     
Files with missing lines Coverage Δ
.../android/flags/internal/ExposureEventsProcessor.kt 100.00% <100.00%> (ø)
...droid/flags/internal/aggregation/AggregationKey.kt 100.00% <100.00%> (ø)
...oid/flags/internal/aggregation/AggregationStats.kt 100.00% <100.00%> (ø)
...flags/internal/aggregation/EvaluationAggregator.kt 97.44% <97.44%> (ø)
...id/flags/internal/net/EvaluationsRequestFactory.kt 75.00% <0.00%> (ø)
...ndroid/flags/internal/EvaluationEventsProcessor.kt 95.16% <95.16%> (ø)

... and 35 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@typotter
typotter force-pushed the typo/FFL-1720-pr1-schema-models branch from c924067 to a031b97 Compare January 28, 2026 06:34
@typotter
typotter force-pushed the typo/FFL-1720-pr2-aggregation branch from 968dc68 to 80c8de7 Compare January 28, 2026 06:55
@typotter
typotter force-pushed the typo/FFL-1720-pr1-schema-models branch 2 times, most recently from ddeaaa3 to 75827d0 Compare January 28, 2026 07:27
@typotter
typotter force-pushed the typo/FFL-1720-pr2-aggregation branch from 2eb16ca to 0cfb30a Compare January 28, 2026 07:27
@typotter
typotter requested a review from dd-oleksii January 28, 2026 07:27
@typotter
typotter force-pushed the typo/FFL-1720-pr2-aggregation branch from 0cfb30a to 141cded Compare January 28, 2026 07:50
@typotter
typotter marked this pull request as ready for review January 28, 2026 07:57
@typotter
typotter requested a review from a team as a code owner January 28, 2026 07:57
@typotter
typotter force-pushed the typo/FFL-1720-pr1-schema-models branch 2 times, most recently from 1c0c827 to 00b8f55 Compare January 28, 2026 16:30
@typotter
typotter force-pushed the typo/FFL-1720-pr2-aggregation branch from 141cded to 4bfc37e Compare January 28, 2026 16:33
@typotter
typotter force-pushed the typo/FFL-1720-pr1-schema-models branch from 00b8f55 to 7dba2fe Compare January 28, 2026 17:01
@typotter
typotter force-pushed the typo/FFL-1720-pr2-aggregation branch from 4bfc37e to 550f9f0 Compare January 28, 2026 17:05
dd-oleksii
dd-oleksii previously approved these changes Feb 11, 2026
0xnm
0xnm previously approved these changes Feb 13, 2026
@typotter

Copy link
Copy Markdown
Contributor Author

/merge

@gh-worker-devflow-routing-ef8351

gh-worker-devflow-routing-ef8351 Bot commented Feb 13, 2026

Copy link
Copy Markdown

View all feedbacks in Devflow UI.

2026-02-13 13:41:33 UTC ℹ️ Start processing command /merge


2026-02-13 13:41:40 UTC ℹ️ MergeQueue: waiting for PR to be ready

This pull request is not mergeable according to GitHub. Common reasons include pending required checks, missing approvals, or merge conflicts — but it could also be blocked by other repository rules or settings.
It will be added to the queue as soon as checks pass and/or get approvals. View in MergeQueue UI.
Note: if you pushed new commits since the last approval, you may need additional approval.
You can remove it from the waiting list with /remove command.


2026-02-13 14:35:12 UTC ℹ️ MergeQueue: merge request added to the queue

The expected merge time in feature/flags-evaluations-logging is approximately 59m (p90).


2026-02-13 15:33:10 UTC ℹ️ MergeQueue: This merge request was merged

Comment on lines +77 to +85
aggregationMap.size
}

if (mapSize < maxAggregations) {
return emptyList()
}

// Re-check inside lock to ensure only one thread drains
val drained = mapLock.withLock {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

minor: given this is the same lock we re-acquire, we don't need to release it and can simply proceed to draining.

Suggested change
aggregationMap.size
}
if (mapSize < maxAggregations) {
return emptyList()
}
// Re-check inside lock to ensure only one thread drains
val drained = mapLock.withLock {

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.

🧑‍🍳

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.

)

if (drainedEvents.isNotEmpty()) {
writer.writeAll(drainedEvents)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

re-schedule periodic flush?

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.

@dd-oleksii dd-oleksii left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks good overall. Added two minor comments above — let's address them in a follow up PR given how long this one is hanging

@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854d Bot merged commit adb81d5 into feature/flags-evaluations-logging Feb 13, 2026
28 checks passed
@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854d Bot deleted the typo/FFL-1720-pr2-aggregation branch February 13, 2026 15:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants