Skip to content

feat(example): add precomputed client demo#241

Open
leoromanovsky wants to merge 15 commits intomainfrom
precomputed/5-example-app
Open

feat(example): add precomputed client demo#241
leoromanovsky wants to merge 15 commits intomainfrom
precomputed/5-example-app

Conversation

@leoromanovsky
Copy link
Member

@leoromanovsky leoromanovsky commented Jan 28, 2026

Motivation

Provide an interactive demonstration of the precomputed client in the example app for testing and development purposes.

Changes

Activity Refactoring:

  • Renamed MainActivityHomeActivity (launcher with client selection)
  • Renamed SecondActivityStandardClientActivity (existing client demo)
  • Added back navigation to both existing activities

New PrecomputedActivity:

  • Subject ID input field
  • Two initialization modes:
    • "From Server" - Fetches fresh configuration from edge endpoint
    • "From Disk" - Uses only cached configuration (offline mode)
  • Dynamic subject attributes table:
    • Add/remove key-value pairs
    • Auto-detects numeric vs string values
    • Pre-populated with platform and appVersion
  • Flag key input with type selector (String, Bool, Int, Num, JSON)
  • "Get Assignment" button to fetch and display results
  • Scrollable log showing all operations
  • Proper lifecycle handling:
    • onPause() → pause polling
    • onResume() → resume polling
    • onDestroy() → stop polling

Layouts:

  • activity_home.xml - Updated with precomputed client button
  • activity_precomputed.xml - New layout with all controls

Resources:

  • Added string for precomputed button
Screen.Recording.2026-02-03.at.5.01.09.PM.mov

Decisions

  • Two init buttons: Makes it easy to test offline vs online scenarios
  • Dynamic attributes: More flexible than hardcoded fields
  • Type selector: Demonstrates all supported flag types
  • Verbose logging: Helps debug issues during development
  • Purple button: Visually distinguishes precomputed option

PR Stack

This PR is part of the precomputed client feature, split for easier review:

main
├── precomputed/1-base-cache-file
│   └─┬─ precomputed/3-storage-layer
│     │   └── precomputed/4-client ⬅ (base)
│     │       └── 👉 precomputed/5-example-app (this PR)
│     │
└── precomputed/2-dtos-and-utils ──────┘

Why this structure: Example app changes are purely additive and don't affect the SDK library. Keeping them separate allows the core feature to be merged and released independently if needed.

Merge order: This is the final PR in the stack. Merge PRs 1-4 first.


Full Stack Summary

PR Title Status
#237 refactor: extract BaseCacheFile Ready
#238 feat: add obfuscation utilities and precomputed DTOs Ready
#239 feat: add precomputed configuration storage Waiting on #237, #238
#240 feat: add EppoPrecomputedClient Waiting on #239
#241 feat(example): add precomputed client demo Waiting on #240

Add the foundational data structures and utilities for the precomputed
client feature:

- ObfuscationUtils: MD5 hashing for flag key obfuscation
- PrecomputedFlag: DTO for precomputed flag assignments
- PrecomputedBandit: DTO for precomputed bandit assignments
- PrecomputedConfigurationResponse: Wire protocol response parsing
- BanditResult: Result container for bandit action lookups
- MissingSubjectKeyException: Validation exception

Includes comprehensive unit tests for serialization round-trips and
MD5 hash consistency.
@leoromanovsky leoromanovsky force-pushed the precomputed/5-example-app branch from 2a2e1bc to 63b0d58 Compare January 28, 2026 16:44
Replace Integer.toHexString() with a pre-computed hex character lookup
table for byte-to-hex conversion. This avoids creating intermediate
String objects for each byte, reducing allocations.

Mirrors optimization from iOS SDK PR #91/#93.
@leoromanovsky leoromanovsky force-pushed the precomputed/5-example-app branch from 63b0d58 to 95a40f0 Compare February 2, 2026 20:10
Add md5HexPrefix() method that only converts the bytes needed for a
given prefix length, avoiding unnecessary work when only a prefix is
required (e.g., cache file naming uses first 8 chars).

Includes unrolled loop for the common 4-byte (8 hex char) case to
help compiler optimization, following iOS SDK PR #93 approach.
Extract common file caching functionality from ConfigCacheFile into
a new BaseCacheFile base class. This enables reuse for the upcoming
precomputed configuration cache without code duplication.

- Add BaseCacheFile with common read/write/delete operations
- Refactor ConfigCacheFile to extend BaseCacheFile
- No functional changes to existing behavior
Add the storage layer for precomputed flag configurations:

- PrecomputedCacheFile: Disk cache file extending BaseCacheFile
- PrecomputedConfigurationStore: In-memory + disk storage with
  async save/load operations and proper thread synchronization
- Updates in-memory config even if disk write fails for resilience

Also adds test data files to Makefile for integration testing.

Includes unit tests for cache operations and failure scenarios.
@leoromanovsky leoromanovsky force-pushed the precomputed/5-example-app branch 2 times, most recently from 328f129 to 5d88392 Compare February 2, 2026 20:14
- Return null consistently in loadConfigFromCache for both file-not-found
  and read-error cases
- Use static EMPTY singleton in PrecomputedConfigurationResponse.empty()
- Use Collections.singletonMap() in getEnvironment() for memory efficiency
Add the main precomputed client implementation with:

- Server-side precomputed flag assignments with instant lookups
- Support for all flag types: string, boolean, integer, numeric, JSON
- Bandit action support with attribute decoding
- Builder pattern with extensive configuration options
- Offline mode with initial configuration support
- Background polling with configurable interval and jitter
- Assignment and bandit logging with deduplication caches
- Graceful error handling mode

The client fetches precomputed assignments from the edge endpoint,
eliminating client-side flag evaluation overhead.

Includes comprehensive instrumented tests covering:
- All flag type assignments
- Assignment logging and deduplication
- Bandit actions
- Offline mode
- SDK test data integration
- Make polling fields volatile for thread safety
- Return user's default value on parse failure instead of hardcoded 0
- Extract magic string to NO_ACTION_CACHE_KEY constant
- Fix banditActions serialization to match JS SDK wire format
- Add comments for hash length and jitter calculation
- Rename misleading test to testNonGracefulModeCanBeConfigured
@leoromanovsky leoromanovsky force-pushed the precomputed/5-example-app branch from 5d88392 to d15bde0 Compare February 3, 2026 20:55
@leoromanovsky leoromanovsky marked this pull request as ready for review February 3, 2026 21:24
Extract environment prefix from SDK key to automatically construct
the correct edge endpoint URL (e.g., https://5qhpgd.fs-edge-assignment.eppo.cloud).
This removes the need for users to manually configure the base URL.
Add demonstration of the precomputed client in the example app:

- Rename MainActivity -> HomeActivity, SecondActivity -> StandardClientActivity
- Add PrecomputedActivity with:
  - Subject ID input with server/disk initialization options
  - Dynamic subject attributes table (add/remove key-value pairs)
  - Flag key input with type selection (string, bool, int, numeric, JSON)
  - Assignment log display
  - Proper lifecycle handling for polling (pause/resume/stop)
- Add back navigation to all activities
- Update layouts and strings

This provides a complete interactive demo of the precomputed client
for testing and development purposes.
@leoromanovsky leoromanovsky force-pushed the precomputed/5-example-app branch from d15bde0 to dd5c86d Compare February 3, 2026 22:06
Address PR review feedback by removing redundant inline comments
that simply restated what the test assertions already conveyed.
Base automatically changed from precomputed/4-client to main February 4, 2026 15:31
@leoromanovsky leoromanovsky enabled auto-merge (squash) February 4, 2026 15:35
Comment on lines +98 to +104
Button removeButton = new Button(this);
removeButton.setText("X");
removeButton.setMinWidth(0);
removeButton.setMinHeight(0);
removeButton.setMinimumWidth(0);
removeButton.setMinimumHeight(0);
removeButton.setPadding(16, 8, 16, 8);

Choose a reason for hiding this comment

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

Is it common to create UI components programmatically like this in Android? 😮

Also .setMinWidth and .setMinimumWidth are different?

Comment on lines +230 to +231
// JSON assignments return JsonNode - for simplicity, we show as string
appendToLog("JSON assignment for '" + flagKey + "': (use getJSONAssignment() API)");

Choose a reason for hiding this comment

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

Could you do

JsonNode jsonResult = precomputedClient.getJSONAssignment(flagKey, null);

and stringify it with jsonResult.toString()?

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