Skip to content

add optional features Data to initializer with API connection#98

Open
vazarkevych wants to merge 21 commits intogrowthbook:mainfrom
vazarkevych:init-with-features
Open

add optional features Data to initializer with API connection#98
vazarkevych wants to merge 21 commits intogrowthbook:mainfrom
vazarkevych:init-with-features

Conversation

@vazarkevych
Copy link
Collaborator

No description provided.

Copy link
Contributor

@madhuchavva madhuchavva left a comment

Choose a reason for hiding this comment

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

It needs to more changes to support default / fallback features incase of internet connection failures.

To add more context, if we just modify the initializer to accept fallbackFeatures along with the API connection, we'll need graceful mechanisms to use these fallbackFeatures and retry the API calls if backgroundSync is turned on.

Nevertheless, Every time we use features to evaluate, first, try regular cache (previously fetched from API) and in case of cache-miss or invalidated scenario, we use these fallback features for evaluation.

We'll need to log these events carefully, otherwise debugging any unexpected behaviors/results will be very tricky.

@vazarkevych
Copy link
Collaborator Author

Hey @madhuchavva, just to clarify - are you proposing that we don’t persist the passed features to the cache, and instead keep them in a separate field, and use them only if cache is empty or invalid?

@vazarkevych vazarkevych requested a review from madhuchavva May 9, 2025 11:49
@vazarkevych
Copy link
Collaborator Author

Hi @madhuchavva, here’s what we’ve done regarding fallback features and connection handling:

  1. We based the changes on the fetch-features branch because it already contains updated logic in the fetchFeatures method. If we hadn't reused that logic, merging both branches later would require not just resolving conflicts but fully rewriting the method. The logic in that branch introduces a cache-first strategy: if the cache is fresh, we use it; if it's stale, we still use it but trigger a background API call. In this branch, we extended that by adding fallback support:

    • if cache is fresh → use it
    • if cache is stale → use it and trigger background fetch
    • if API fetch fails → continue using stale cache
    • if no cache exists → use fallbackFeatures
  2. We also implemented automatic connection recovery. As soon as internet becomes available again, and if backgroundSync is enabled, the SDK immediately triggers a remote API call to fetch features. If backgroundSync is off, the next time features are accessed, a fresh API call will be made to update them.

Could you please review and let us know if this logic looks good to you or if you'd suggest any improvements?

Volodymyr Nazarkevych and others added 10 commits July 28, 2025 15:09
# Conflicts:
#	Sources/CommonMain/Features/FeaturesViewModel.swift
#	Sources/CommonMain/GrowthBookSDK.swift
# Conflicts:
#	Sources/CommonMain/Features/FeaturesViewModel.swift
#	Sources/CommonMain/GrowthBookSDK.swift
# Conflicts:
#	GrowthBook-IOS.xcodeproj/project.pbxproj
#	GrowthBookTests/ExperimentRunTests.swift
#	GrowthBookTests/FeaturesViewModelTests.swift
#	GrowthBookTests/GrowthBookSDKBuilderTests.swift
#	GrowthBookTests/Source/json.json
#	Sources/CommonMain/Caching/CachingManager.swift
#	Sources/CommonMain/Evaluators/ConditionEvaluator.swift
#	Sources/CommonMain/Evaluators/ExperimentEvaluator.swift
#	Sources/CommonMain/Evaluators/FeatureEvaluator.swift
#	Sources/CommonMain/Features/FeaturesViewModel.swift
#	Sources/CommonMain/GrowthBookSDK.swift
#	Sources/CommonMain/Model/Context.swift
#	Sources/CommonMain/Model/Feature.swift
#	Sources/CommonMain/Model/GlobalContext.swift
#	Sources/CommonMain/Model/StickyAssignmentsDocument.swift
#	Sources/CommonMain/StickyBucket/StickyBucketService.swift
#	Sources/CommonMain/Utils/Constants.swift
#	Sources/CommonMain/Utils/Utils.swift

# Conflicts:
#	GrowthBookTests/FeaturesViewModelTests.swift
#	GrowthBookTests/GrowthBookSDKBuilderTests.swift
#	Sources/CommonMain/Caching/CachingManager.swift
#	Sources/CommonMain/Evaluators/ExperimentEvaluator.swift
#	Sources/CommonMain/Evaluators/FeatureEvaluator.swift
#	Sources/CommonMain/Features/FeaturesViewModel.swift
#	Sources/CommonMain/GrowthBookSDK.swift
#	Sources/CommonMain/Model/Feature.swift
#	Sources/CommonMain/Model/StickyAssignmentsDocument.swift
#	Sources/CommonMain/StickyBucket/StickyBucketService.swift
#	Sources/CommonMain/Utils/Constants.swift
#	Sources/CommonMain/Utils/Utils.swift
# Conflicts:
#	GrowthBookTests/FeaturesViewModelTests.swift
#	GrowthBookTests/GrowthBookSDKBuilderTests.swift
#	Sources/CommonMain/Features/FeaturesViewModel.swift
#	Sources/CommonMain/GrowthBookSDK.swift
#	Sources/CommonMain/Model/Context.swift
#	Sources/CommonMain/Utils/Constants.swift
#	Sources/CommonMain/Utils/Utils.swift
Copy link
Contributor

@madhuchavva madhuchavva left a comment

Choose a reason for hiding this comment

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

I reviewed these changes thoroughly but couldn't run the tests though! Here are my findings.

  1. [P1] Fallback can override a valid cached feature set (wrong precedence).
    In fetchFeatures, cached features are loaded first, but if the network call fails and fallbackFeatures exists, fallback is applied unconditionally, replacing cache-derived features. This conflicts with the intended order “cache first, fallback only on cache miss/invalid”.
    See the lines : FeaturesViewModel.swift#L101, FeaturesViewModel.swift#L120, FeaturesViewModel.swift#L130

  2. [P1] @objc initializer API is source-breaking for Objective-C consumers.
    Adding fallbackFeatures in the middle of the existing @objc initializer changes the generated Objective-C selector. Existing ObjC call sites using the old selector will fail to compile. This should be introduced via an overload (or appended parameter strategy), not by changing the existing selector shape.
    See the lines : GrowthBookSDK.swift#L51, GrowthBookSDK.swift#L56

  3. [P2] Fallback payload parsing is too narrow and silently drops valid API-shaped data.
    features initialization supports both FeaturesDataModel and raw Features, but fallbackFeatures only decodes raw Features. If callers pass API-style JSON, fallback becomes nil with no warning, making failures hard to diagnose.
    References: GrowthBookSDK.swift#L257, GrowthBookSDK.swift#L291, GrowthBookSDK.swift#L293

  4. Possibly a missing behavior gap - [P2] Remote-eval failure path ignores fallback/retry semantics.
    In remoteEval mode, failures only emit .failedToLoadData; there is no fallback usage and no retry/backoff behavior tied to backgroundSync. This leaves the “graceful offline/default behavior” incomplete for one of the main fetch paths.
    References: FeaturesViewModel.swift#L103, FeaturesViewModel.swift#L109

I believe the points 1 and 2 are regressions. Happy to discuss more if you have any questions.

# Conflicts:
#	Sources/CommonMain/Caching/CachingManager.swift
#	Sources/CommonMain/GrowthBookSDK.swift
#	Sources/CommonMain/Utils/Utils.swift
@vazarkevych
Copy link
Collaborator Author

vazarkevych commented Mar 5, 2026

I reviewed these changes thoroughly but couldn't run the tests though! Here are my findings.

  1. [P1] Fallback can override a valid cached feature set (wrong precedence).
    In fetchFeatures, cached features are loaded first, but if the network call fails and fallbackFeatures exists, fallback is applied unconditionally, replacing cache-derived features. This conflicts with the intended order “cache first, fallback only on cache miss/invalid”.
    See the lines : FeaturesViewModel.swift#L101, FeaturesViewModel.swift#L120, FeaturesViewModel.swift#L130
  2. [P1] @objc initializer API is source-breaking for Objective-C consumers.
    Adding fallbackFeatures in the middle of the existing @objc initializer changes the generated Objective-C selector. Existing ObjC call sites using the old selector will fail to compile. This should be introduced via an overload (or appended parameter strategy), not by changing the existing selector shape.
    See the lines : GrowthBookSDK.swift#L51, GrowthBookSDK.swift#L56
  3. [P2] Fallback payload parsing is too narrow and silently drops valid API-shaped data.
    features initialization supports both FeaturesDataModel and raw Features, but fallbackFeatures only decodes raw Features. If callers pass API-style JSON, fallback becomes nil with no warning, making failures hard to diagnose.
    References: GrowthBookSDK.swift#L257, GrowthBookSDK.swift#L291, GrowthBookSDK.swift#L293
  4. Possibly a missing behavior gap - [P2] Remote-eval failure path ignores fallback/retry semantics.
    In remoteEval mode, failures only emit .failedToLoadData; there is no fallback usage and no retry/backoff behavior tied to backgroundSync. This leaves the “graceful offline/default behavior” incomplete for one of the main fetch paths.
    References: FeaturesViewModel.swift#L103, FeaturesViewModel.swift#L109

I believe the points 1 and 2 are regressions. Happy to discuss more if you have any questions.

Hi @madhuchavva
Thanks for the review!

  • Fallback now only applies when cacheLoaded = false
  • fallbackFeatures moved to a separate builder method setFallbackFeatures() — existing @objc selectors unchanged
  • Fallback parsing now tries both FeaturesDataModel and raw Features with an error log on failure
  • Added fallback to remoteEval failure path.

@vazarkevych vazarkevych requested a review from madhuchavva March 5, 2026 19:31
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.

3 participants