Skip to content

feat: SDK-4417: emit ossdk.features_enabled in Otel per-event payload#2631

Open
abdulraqeeb33 wants to merge 6 commits intomainfrom
ar/sdk-4417-feature-flag-otel-payload
Open

feat: SDK-4417: emit ossdk.features_enabled in Otel per-event payload#2631
abdulraqeeb33 wants to merge 6 commits intomainfrom
ar/sdk-4417-feature-flag-otel-payload

Conversation

@abdulraqeeb33
Copy link
Copy Markdown
Contributor

@abdulraqeeb33 abdulraqeeb33 commented Apr 29, 2026

Description

One Line Summary

SDK emits the canonical keys of currently-enabled feature flags as a per-event Otel attribute (ossdk.features_enabled) so logs are queryable by feature flag in Google Cloud Logs Explorer.

Details

Motivation

SDK-4417. When debugging SDK behavior from Otel logs, we need to know which feature flags were active at the moment a record was written. Without this, a regression that only manifests when a flag is on (e.g. sdk_background_threading) is hard to pinpoint from logs alone.

Scope

  • Adds one new per-event Otel log attribute: ossdk.features_enabled — a sorted, comma-separated list of canonical flag keys; omitted entirely when no flags are enabled. Example value: "sdk_background_threading".
  • Read on every emission so each record reflects the live FeatureManager.featureStates snapshot. Empty string is never written; absent attribute = no flags enabled.
  • No public API change. No effect on apps that don't have remote logging enabled (no records emitted = no extra payload). For apps that do, the added bytes per record are minimal (gzip compresses repeated keys aggressively).
  • KMP-friendly: the Otel module remains platform-independent. IOtelPlatformProvider.enabledFeatureFlags is the platform-agnostic contract; the Android impl delegates to IFeatureManager. iOS or other platforms can implement their own source.

Architecture

Strict constructor injection — no late-bound state, no @Volatile var, no setters:

OneSignalImp.initEssentials
  -> ensureApplicationServiceStarted(ctx)        // enables FeatureManager resolution
  -> services.getService<IFeatureManager>()
  -> new OtelLifecycleManager(ctx, featureManager)
       -> platformProviderFactory(ctx, featureManager)
            -> new OtelPlatformProvider(config, featureManager)
                 -> enabledFeatureFlags reads featureManager.enabledFeatureKeys() per call

OtelFieldsPerEvent.getAttributes() reads platformProvider.enabledFeatureFlags on every log emission, so IMMEDIATE-mode flag changes show up on the very next record without rebuilding the Otel SDK.

Querying example

In Google Cloud Logs Explorer:

jsonPayload.attributes."ossdk.features_enabled":"sdk_background_threading"

The : operator does substring contains; flag keys are unique full identifiers, so substring matching has no false positives.

Testing

Unit testing

  • FeatureManagerTests — added coverage for new enabledFeatureKeys(): empty by default, returns canonical key when enabled, respects APP_STARTUP latching after onModelReplaced.
  • OtelFieldsPerEventTest — added cases for empty, single flag, multiple sorted, and "re-read on every call" (per-event semantics).
  • OtelFieldsTopLevelTest — added negative case confirming ossdk.features_enabled is not at top level.
  • OtelPlatformProviderTest — three cases: empty when manager has no flags, fresh per-access from injected manager, empty when manager throws.
  • OtelLifecycleManagerTest — verifies IFeatureManager is forwarded to the platform provider via the factory.
  • OtelLifecycleManagerFaultTest, OneSignalCrashHandlerFactoryTest, OneSignalCrashUploaderWrapperTest, OtelIntegrationTest — updated to thread the new constructor parameter through.

All tests in the affected suites pass; pre-existing SDKInitTests failures on main are unrelated.

Manual testing

Not yet exercised end-to-end against a staging app. The wire format is straightforward (one extra string attribute on each LogRecord), and unit tests cover the resolution path. Worth a smoke test once merged.

Affected code checklist

  • Notifications
    • Display
    • Open
    • Push Processing
    • Confirm Deliveries
  • Outcomes
  • Sessions
  • In-App Messaging
  • REST API requests
  • Public API changes

(Otel/observability is not in the affected-code list — this PR only touches the Otel pipeline and feature flag plumbing.)

Checklist

Overview

  • I have filled out all REQUIRED sections above
  • PR does one thing
  • Any Public API changes are explained in the PR details and conform to existing APIs (none in this PR)

Testing

  • I have included test coverage for these changes, or explained why they are not needed
  • All automated tests pass, or I explained why that is not possible (unrelated pre-existing SDKInitTests failures on main)
  • I have personally tested this on my device, or explained why that is not possible (still need an end-to-end smoke test)

Final pass

  • Code is as readable as possible
  • I have reviewed this PR myself

Adds a per-event Otel attribute carrying the canonical keys of feature
flags currently enabled for this device (encoded as a sorted, comma-
separated string; omitted when empty). Logs become easily queryable in
Google Cloud Logs Explorer, e.g.:

  jsonPayload.attributes."ossdk.features_enabled":"sdk_background_threading"

Read fresh on every emission so each record reflects FeatureManager's
current featureStates snapshot — no SDK rebuild required for IMMEDIATE-
mode flag changes, no late-bound state.

- otel module: IOtelPlatformProvider.enabledFeatureFlags exposes the
  list as a platform-agnostic contract; OtelFieldsPerEvent encodes it.
- core module (Android): IFeatureManager gains enabledFeatureKeys();
  OtelPlatformProvider takes IFeatureManager via constructor injection
  (no @volatile var, no setter). OtelLifecycleManager and
  OneSignalCrashHandlerFactory thread it through.
- OneSignalImp.initEssentials reorders so ensureApplicationServiceStarted
  runs before resolving FeatureManager from the IoC container.

Made-with: Cursor
Copilot AI review requested due to automatic review settings April 29, 2026 21:59
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds per-event OpenTelemetry log context for active feature flags so remote logs can be queried by flag state (SDK-4417).

Changes:

  • Adds IFeatureManager.enabledFeatureKeys() and threads IFeatureManager through Otel lifecycle/platform provider creation.
  • Extends IOtelPlatformProvider with enabledFeatureFlags and emits enabled flags in OtelFieldsPerEvent as a sorted, comma-separated attribute.
  • Updates/extends unit tests across Otel + crash plumbing to validate per-event semantics and injection.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/attributes/OtelFieldsTopLevelTest.kt Adds negative test ensuring the feature-flags attribute is not top-level.
OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/attributes/OtelFieldsPerEventTest.kt Adds tests for omit/encode/sort and per-call re-read behavior.
OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/attributes/OtelFieldsPerEvent.kt Emits enabled feature flags as a per-event attribute (key currently mismatched vs spec/tests).
OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/IOtelPlatformProvider.kt Adds a new enabledFeatureFlags contract for platform providers.
OneSignalSDK/onesignal/core/src/test/java/com/onesignal/internal/OtelLifecycleManagerTest.kt Updates lifecycle tests for new IFeatureManager injection and factory forwarding.
OneSignalSDK/onesignal/core/src/test/java/com/onesignal/internal/OtelLifecycleManagerFaultTest.kt Threads IFeatureManager through factories and verifies fault tolerance still holds.
OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/logging/otel/android/OtelPlatformProviderTest.kt Updates provider creation and adds tests for enabledFeatureFlags.
OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/crash/OtelIntegrationTest.kt Updates integration tests to pass a feature manager into provider creation.
OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/crash/OneSignalCrashUploaderWrapperTest.kt Updates feature-manager mocks to support enabledFeatureKeys().
OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/crash/OneSignalCrashHandlerFactoryTest.kt Updates factory tests for new featureManager parameter.
OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/features/FeatureManagerTests.kt Adds unit coverage for enabledFeatureKeys() and activation-mode behavior.
OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OtelLifecycleManager.kt Requires IFeatureManager and passes it into crash handler + platform provider factories.
OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalImp.kt Ensures app service starts before resolving IFeatureManager, injects it into OtelLifecycleManager.
OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/android/OtelPlatformProvider.kt Adds per-access enabledFeatureFlags backed by IFeatureManager.enabledFeatureKeys().
OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OneSignalCrashUploaderWrapper.kt Passes featureManager into platform provider creation.
OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OneSignalCrashHandlerFactory.kt Adds featureManager param and injects it into platform provider creation.
OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/features/FeatureManager.kt Implements enabledFeatureKeys() (but currently enables a local override by default).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

// operator; omitted entirely when no flags are enabled to keep payloads compact.
val enabledFlags = platformProvider.enabledFeatureFlags
if (enabledFlags.isNotEmpty()) {
attributes["ossdk.feature_flags"] = enabledFlags.sorted().joinToString(",")
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

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

OtelFieldsPerEvent is writing the enabled flags under the attribute key ossdk.feature_flags, but the PR (and the accompanying tests) specify the canonical per-event key as ossdk.features_enabled. With the current key, the new tests will fail and log queries/documentation will not match. Please rename the attribute key to ossdk.features_enabled (and keep the omit-when-empty behavior).

Suggested change
attributes["ossdk.feature_flags"] = enabledFlags.sorted().joinToString(",")
attributes["ossdk.features_enabled"] = enabledFlags.sorted().joinToString(",")

Copilot uses AI. Check for mistakes.
Comment on lines +175 to +177
// private val localFeatureOverrides: Set<String> = emptySet()
private val localFeatureOverrides: Set<String> =
setOf(FeatureFlag.SDK_BACKGROUND_THREADING.key)
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

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

localFeatureOverrides is currently hard-coded to setOf(FeatureFlag.SDK_BACKGROUND_THREADING.key), which will force-enable background threading for all builds and environments. This looks like the local testing hook described in the comment above and should not be enabled by default. Please revert this to emptySet() (or gate it behind a debug-only build flag) so production behavior is driven solely by remote config / activation mode rules.

Suggested change
// private val localFeatureOverrides: Set<String> = emptySet()
private val localFeatureOverrides: Set<String> =
setOf(FeatureFlag.SDK_BACKGROUND_THREADING.key)
private val localFeatureOverrides: Set<String> = emptySet()

Copilot uses AI. Check for mistakes.
Comment on lines +41 to +45
@@ -41,20 +42,21 @@ import com.onesignal.otel.crash.IOtelAnrDetector
@Suppress("TooManyFunctions")
internal class OtelLifecycleManager(
private val context: Context,
private val crashHandlerFactory: (Context, IOtelLogger) -> IOtelCrashHandler =
{ ctx, log -> OneSignalCrashHandlerFactory.createCrashHandler(ctx, log) },
private val featureManager: IFeatureManager,
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

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

The class KDoc still says production callers can use OtelLifecycleManager(context), but the constructor now requires an IFeatureManager. Please update this documentation to reflect the new required parameter (or adjust the example to show how it’s obtained/injected).

Copilot uses AI. Check for mistakes.
Comment on lines +243 to +244
// FeatureManager is constructor-injected into OtelLifecycleManager so OTel resource
// attributes (ossdk.features_enabled) reflect applied flag state from the very first
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

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

This comment refers to ossdk.features_enabled as an "OTel resource" attribute, but the implementation and PR description treat it as a per-event/log-record attribute. Please adjust the wording here to avoid confusion about where the attribute is emitted (resource vs per-event payload).

Suggested change
// FeatureManager is constructor-injected into OtelLifecycleManager so OTel resource
// attributes (ossdk.features_enabled) reflect applied flag state from the very first
// FeatureManager is constructor-injected into OtelLifecycleManager so per-log/event
// attributes like ossdk.features_enabled reflect applied flag state from the very first

Copilot uses AI. Check for mistakes.
* The order is not guaranteed; consumers that need a deterministic encoding (e.g. for
* stable log payloads) should sort before serializing.
*/
val enabledFeatureFlags: List<String>
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

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

IOtelPlatformProvider is a public interface in the published onesignal-otel module, and this PR adds a new abstract member (val enabledFeatureFlags). That is a breaking API/binary change for any downstream implementers of IOtelPlatformProvider (e.g., other platform/KMP implementations). If this interface is intended to be internal-only, consider marking it internal; otherwise, consider providing a default implementation (or updating the PR description to acknowledge the public contract change).

Suggested change
val enabledFeatureFlags: List<String>
val enabledFeatureFlags: List<String>
get() = emptyList()

Copilot uses AI. Check for mistakes.
Comment on lines +175 to +177
// private val localFeatureOverrides: Set<String> = emptySet()
private val localFeatureOverrides: Set<String> =
setOf(FeatureFlag.SDK_BACKGROUND_THREADING.key)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Local feature override accidentally committed: the production-safe emptySet() is commented out in favor of setOf(FeatureFlag.SDK_BACKGROUND_THREADING.key) (FeatureManager.kt:175-177), force-enabling SDK_BACKGROUND_THREADING for 100% of users on the released SDK. This bypasses the remote-config canary/percentage rollout for a high-risk threading-mode flag and is unrelated to this PR's OTel goal — please revert the override (re-comment the setOf line, restore emptySet()) before merge.

Extended reasoning...

What is wrong

In FeatureManager.kt:175-177, the localFeatureOverrides companion-object constant has been flipped from the production-safe value to the test-only override. The diff in this PR is:

-        private val localFeatureOverrides: Set<String> = emptySet()
-//        private val localFeatureOverrides: Set<String> =
-//            setOf(FeatureFlag.SDK_BACKGROUND_THREADING.key)
+//        private val localFeatureOverrides: Set<String> = emptySet()
+        private val localFeatureOverrides: Set<String> =
+            setOf(FeatureFlag.SDK_BACKGROUND_THREADING.key)

The KDoc immediately above the constant (lines 170–174) explicitly labels it as a "Local-only test hook for forcing features ON without backend config. Add feature keys here while testing locally". The previous state had emptySet() active and the override commented out — the PR inverted them, so the test override is now the live binding shipped to users.

Why the existing code does not prevent it

refreshEnabledFeatures (same file) unions localFeatureOverrides into enabledFeatureKeys unconditionally on every refresh — there is no BuildConfig.DEBUG guard, no debuggable-package check, and no environment gate. The override applies in any build, including released production. Because SDK_BACKGROUND_THREADING is APP_STARTUP-latched, the flip is captured at the first refresh and then persisted for the entire session; remote config cannot turn it OFF mid-session.

The Logging.warn("OneSignal: Local feature override enabled for testing only: …") line in refreshEnabledFeatures will also fire on every device on every refresh, polluting production logs.

Step-by-step proof

  1. App with this SDK build starts. FeatureManager.<init> calls refreshEnabledFeatures(configModelStore.model, applyNextRunOnlyFeatures = true).
  2. enabledFeatureKeys is computed as model.sdkRemoteFeatureFlags.map(canonicalize) + localFeatureOverrides.map(canonicalize). With this diff, localFeatureOverrides = setOf("sdk_background_threading"), so "sdk_background_threading" is added regardless of what the backend returned.
  3. localFeatureOverrides.isNotEmpty() is true → Logging.warn("…enabled for testing only: [sdk_background_threading]") fires on every device.
  4. Loop over FeatureFlag.entries. For SDK_BACKGROUND_THREADING: desiredState = isEnabledIn(enabledFeatureKeys) = true. activationMode = APP_STARTUP, applyNextRunOnlyFeatures = truenextStates[SDK_BACKGROUND_THREADING] = true and applySideEffects(...) calls ThreadingMode.updateUseBackgroundThreading(enabled = true, …).
  5. The flag stays latched true for the lifetime of the process. Subsequent remote-config refreshes call refreshEnabledFeatures(..., applyNextRunOnlyFeatures = false), which for APP_STARTUP-mode flags only logs but does not change the runtime state — so even a backend rollback cannot disable it for an already-running device.

Impact

  • Every device running this released SDK has SDK_BACKGROUND_THREADING force-enabled at startup, regardless of the backend rollout.
  • The canary / percentage rollout system that exists specifically for high-risk flags like this one (a threading-mode change) is fully bypassed.
  • The "testing only" warning log is emitted on every device, every refresh.
  • This is unrelated to the PRs stated goal of emitting ossdk.features_enabled in the per-event OTel payload — the new OTel attribute reads from enabledFeatureKeys(), which would correctly reflect remote config alone if localFeatureOverrides = emptySet().

Fix

Revert the override before merge: re-comment the setOf(...) line and uncomment the emptySet() line, so lines 175-177 read:

        private val localFeatureOverrides: Set<String> = emptySet()
//        private val localFeatureOverrides: Set<String> =
//            setOf(FeatureFlag.SDK_BACKGROUND_THREADING.key)

This is almost certainly a developers local testing setup that was committed by accident.

Comment on lines +34 to +36
val enabledFlags = platformProvider.enabledFeatureFlags
if (enabledFlags.isNotEmpty()) {
attributes["ossdk.feature_flags"] = enabledFlags.sorted().joinToString(",")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 The new per-event attribute is emitted under the key ossdk.feature_flags (OtelFieldsPerEvent.kt:36) but the PR description, doc comments, and every new test assertion use ossdk.features_enabled. This breaks the three new positive tests in OtelFieldsPerEventTest.kt (they look up the missing key and get null) and the documented Logs Explorer query jsonPayload.attributes."ossdk.features_enabled":"..." will never match anything in production. Rename the literal on line 36 to ossdk.features_enabled to match the spec/tests.

Extended reasoning...

Attribute key mismatch

The production code emits the new per-event attribute under one name while the entire rest of the PR — tests, doc comments, PR description, and the example Logs Explorer query — uses a different name.

Where it diverges:

  • OtelFieldsPerEvent.kt:36 writes attributes["ossdk.feature_flags"] = enabledFlags.sorted().joinToString(",")
  • All other references — including the doc comments at OneSignalImp.kt:244, OtelPlatformProvider.kt:68, and OneSignalCrashHandlerFactory.kt:16,26 — and the new test assertions at OtelFieldsPerEventTest.kt:85,99,113,116 and OtelFieldsTopLevelTest.kt:85 — use "ossdk.features_enabled".
  • The PR title and description (including the GCP Logs Explorer example query jsonPayload.attributes."ossdk.features_enabled":"sdk_background_threading") also use features_enabled.

A grep across the repo shows "ossdk.feature_flags" appears only on the one production line introduced by this PR; "ossdk.features_enabled" appears everywhere else.

Why existing code does not prevent it: the value is simply written to a MutableMap<String, String>. There is no compile-time check that the key matches the test/spec name; the typo passes the type checker.

Impact:

  1. The three new positive tests in OtelFieldsPerEventTest.kt will fail at runtime. For example, on line 85 the test asserts attributes["ossdk.features_enabled"] shouldBe "sdk_background_threading", but production only ever writes attributes["ossdk.feature_flags"]. The map lookup of the wrong key returns null, so the kotest assertion null shouldBe "sdk_background_threading" fails. The same failure applies to the multi-flag-sorted test and the per-event re-read test. (The empty-flags test happens to pass only because neither key is set.)
  2. The negative test in OtelFieldsTopLevelTest.kt:85 still passes (it asserts the per-event-only key is not at the top level), but it asserts the wrong key for what production actually emits.
  3. The PRs stated queryability goal is broken. The documented query jsonPayload.attributes."ossdk.features_enabled":"sdk_background_threading" would never match an emitted record because the actual attribute name in production logs is ossdk.feature_flags. This silently defeats the entire SDK-4417 motivation.

Step-by-step proof:

  1. App enables SDK_BACKGROUND_THREADING (the default in this PR — see the uncommented localFeatureOverrides in FeatureManager.kt).
  2. OtelFieldsPerEvent.getAttributes() runs on a log emission.
  3. enabledFlags = platformProvider.enabledFeatureFlags returns ["sdk_background_threading"].
  4. Code path enters the if (enabledFlags.isNotEmpty()) branch.
  5. The map gets attributes["ossdk.feature_flags"] = "sdk_background_threading".
  6. The test in OtelFieldsPerEventTest.kt:85 runs attributes["ossdk.features_enabled"] shouldBe "sdk_background_threading". Map lookup of "ossdk.features_enabled" returns null (the only key set is "ossdk.feature_flags"). The assertion null shouldBe "sdk_background_threading" fails the test.
  7. In production, the emitted log record contains jsonPayload.attributes."ossdk.feature_flags": "sdk_background_threading". A user running the documented Logs Explorer query jsonPayload.attributes."ossdk.features_enabled":"sdk_background_threading" matches zero records.

Fix: change the literal on OtelFieldsPerEvent.kt:36 from "ossdk.feature_flags" to "ossdk.features_enabled". Tests, doc comments, and the spec are already internally consistent on features_enabled, so a single one-character edit aligns the production code with the rest of the PR.

AR Abdul Azeez added 3 commits April 29, 2026 18:11
Replaces the constructor-injected IFeatureManager with a () -> IFeatureManager
supplier so the OTel pipeline can come up before service bootstrap completes.
The supplier is an immutable constructor val (no @volatile, no setter); it's
invoked per-event when reading enabledFeatureFlags, which is when services
are guaranteed to be ready.

- OtelPlatformProvider: featureManager -> featureManagerProvider lambda;
  enabledFeatureFlags getter calls it on every access.
- OtelLifecycleManager + OneSignalCrashHandlerFactory: signatures updated to
  thread the supplier through.
- OneSignalImp.initEssentials: restored to its original order
  (OtelLifecycleManager constructed first so crash/log capture starts early,
  ensureApplicationServiceStarted at the end). Wires the supplier as
  { services.getService<IFeatureManager>() }.
- OneSignalCrashUploaderWrapper: forwards its existing IFeatureManager via
  a closure { featureManager } supplier.
- Reverts a stray local-test toggle of FeatureManager.localFeatureOverrides
  that snuck into the previous commit.
- Fixes a doc typo: ossdk.features_flags -> ossdk.features_enabled.

Tests updated to lambda form (createAndroidOtelPlatformProvider(ctx) { fm }).

Made-with: Cursor
Aligns the per-event attribute name with the more standard
"feature_flags" naming (was inconsistently called features_enabled in
tests and docs while the impl already used feature_flags).

Query example:
  jsonPayload.attributes."ossdk.feature_flags":"sdk_background_threading"

Made-with: Cursor
Two doc/contract fixes from Copilot's review on PR #2631:

- OtelLifecycleManager class KDoc: previously described the production
  constructor as `OtelLifecycleManager(context)`, but it now requires a
  featureManagerProvider supplier. Updated example to reflect the new
  signature.

- IOtelPlatformProvider.enabledFeatureFlags: provided a default getter
  returning emptyList() so adding this property is no longer a binary-
  breaking change for downstream KMP / platform implementers. Platforms
  that want to populate `ossdk.feature_flags` opt in by overriding.

The two larger review findings (accidental localFeatureOverrides flip,
attribute-key mismatch) were already addressed in earlier commits on
this branch.

Made-with: Cursor
@nan-li nan-li self-requested a review April 30, 2026 16:34
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.

4 participants