Skip to content

FFL-3565: Accept every numeric type in Int and Double reads - #4099

Open
aarsilv wants to merge 7 commits into
developfrom
aarsilv/ffl-3565/numeric-type-mismatch
Open

aarsilv wants to merge 7 commits into
developfrom
aarsilv/ffl-3565/numeric-type-mismatch

Conversation

@aarsilv

@aarsilv aarsilv commented Nov 3, 2026 •

Copy link
Copy Markdown

What does this PR do?

This PR makes Int reads of numeric flags lossless, and checks whole values in Double reads (FFL-3565). The served numeric type ("integer", "number" or "float") does not change the result:

  • An Int read returns the value when the SDK parses a whole number in Int range.
  • An Int read returns TYPE_MISMATCH when the parsed value is fractional or out of Int range.
  • An Int read returns PARSE_ERROR when the SDK cannot parse the value. The SDK does not parse a value longer than 64 characters unless it is a plain integer in Int range.
  • A Double read returns a fractional value as the nearest Double, such as 1.2.
  • A Double read returns a whole value only when the Double has exactly that value, or prints as that value, such as 1e300. Otherwise it returns TYPE_MISMATCH, for example for 9007199254740993.
  • A Double read returns PARSE_ERROR for NaN and Infinity, and for a value longer than 64 characters whose Double is 2^53 or more in magnitude.

Int reads, compared with develop:

Value develop: "integer", "number" develop: "float" This PR: all numeric types
5 value TYPE_MISMATCH value
5.0, 1e3 PARSE_ERROR TYPE_MISMATCH value
1.2 PARSE_ERROR TYPE_MISMATCH TYPE_MISMATCH
2147483648 PARSE_ERROR TYPE_MISMATCH TYPE_MISMATCH
not-a-number PARSE_ERROR TYPE_MISMATCH PARSE_ERROR

Double reads, compared with develop (all numeric types):

Value develop This PR
1.2, 5, 9007199254740992, 1e300 value value
9007199254740993, 9223372036854775807 rounded value, such as 9.007199254740992E15 TYPE_MISMATCH
1e400 Infinity TYPE_MISMATCH
NaN, Infinity, 1e2147483649 NaN or Infinity PARSE_ERROR
Text longer than 64 characters whose parsed Double is 2^53 or more in magnitude the parsed Double PARSE_ERROR

Each error returns the default value.

Motivation

The OpenFeature spec defines a Number as "a numeric value of unspecified type or size". This PR uses lossless Int conversion and checks whole values in Double reads, for all three numeric types. Requirement 1.3.4 recommends the default value when a typed read gets a value of an unexpected type. The flagd numeric coercion ADR describes lossless numeric coercion, with a separate limit of 2^53−1. This PR does not add that limit.

Additional Notes

Release notes

  • Int reads change as the table shows. Evaluation events show the new error codes.
  • resolve() and the OpenFeature provider do not log TYPE_MISMATCH. For parsed fractional or out-of-range "integer" or "number" values, these paths no longer log a PARSE_ERROR warning. resolveIntValue logs a TYPE_MISMATCH warning.
  • Int reads of "float" values that the SDK cannot parse now log a PARSE_ERROR warning on all paths.
  • Reads that now return a value can send exposure and RUM evaluation events. The usual settings apply.
  • For parsed fractional or out-of-range "integer" or "number" values, OpenFeature error hooks now get TYPE_MISMATCH instead of PARSE_ERROR.
  • An Int read of a whole "float" value in Int range now returns the value. So resolveIntValue no longer logs a TYPE_MISMATCH warning for it.
  • New TYPE_MISMATCH messages include the value. Example: Flag has type 'number' with fractional value '1.2' but Int was requested.
  • At 2^53 or more in magnitude, a Double read of a whole value that matches neither the Double nor its printed form, such as 9007199254740993, now returns the default value with TYPE_MISMATCH instead of a rounded value.
  • Double reads of NaN or Infinity now return PARSE_ERROR. 1e400 now returns TYPE_MISMATCH instead of Infinity.
  • New Double errors appear in evaluation events and OpenFeature error hooks. resolveDoubleValue logs a TYPE_MISMATCH warning, and every read path logs a PARSE_ERROR warning. Failed Double reads send no exposure or RUM evaluation events.
  • At 2^53 or more in magnitude, a Double read of a spelling that BigDecimal rejects, such as one with surrounding spaces, now returns PARSE_ERROR. This comes from reading the source; no test checks it.
  • We plan to add the same rule to the iOS SDK and the server SDKs in separate changes. Until then, the same Int read can give a different result on Android.

Implementation

  • All Int reads of numeric flags use parseLosslessInt() in the runCatching block of convert().
  • BigDecimal finds fractional values and checks the Int range. It does not round.
  • isFractional() checks the scale before it calls stripTrailingZeros(). This prevents a scale overflow for values such as 100e2147483647.
  • Int reads first try toIntOrNull(). If that fails, a value longer than 64 characters returns PARSE_ERROR. This limit bounds the BigDecimal work.
  • Double reads use parseLosslessDouble(). Below 2^53 in magnitude, every whole value is exact, so the parsed Double returns with no other check. At 2^53 or more, BigDecimal checks that a whole value equals the Double or its printed form (BigDecimal.valueOf). The 64-character limit applies there too.

Documentation

  • The FlagsClient.resolveIntValue and FlagsClient.resolveDoubleValue KDoc and the Flags README describe the Int-read and Double-read rules and the 64-character limit.
  • The README now defines TYPE_MISMATCH as a flag value type that does not match the expected type. Its example is a fractional value read with resolveIntValue.
  • No other documents change.

Validation

  • Flags module: ktlintFormat, detekt and lintRelease pass. testDebugUnitTest: 856 tests, 0 failures.
  • OpenFeature module testDebugUnitTest: 84 tests, 0 failures.
  • :reliability:single-fit:flags:testReleaseUnitTest: 155 tests, 0 failures. The new single-fit checks fail with the develop converter (3 failures).
  • checkGeneratedFiles passes.
  • Mutants: each mutant causes test failures. The mutants accept "string" as numeric, remove the length limit, the lower Int bound or the scale check, or move the length limit by one or two characters.
  • Double mutants: each causes test failures. They remove the exactness check, remove the printed-form check, remove the length limit, or include 2^53 in the fast path.
  • Shared fixtures: DataDog/ffe-system-test-data#65, with the fixture runner from FFL-3568: Share precomputed fixtures and lossless numeric policy #4101, on this branch merged with develop. The fixtures omit extraLogging, which the Android model requires, so the test copy adds an empty object to each assignment. Of 95 runs, 88 pass, 3 are skipped and 4 fail. The 4 failures expect FLAG_NOT_FOUND for a flag with an unsupported variationType, which FFL-3568: Share precomputed fixtures and lossless numeric policy #4101 handles. Without the Double change, 12 more runs fail: the Double reads of whole values above 2^53.
  • Android check: BigDecimal("0.0").stripTrailingZeros() returns scale 0 on API 28 (build PSR1.210301.009.B1) and API 36 emulators. The AOSP libcore source for Android 6 to 9 returns a zero unchanged, with its scale, so the API 28 emulator differs from that source. API 23 to 27 were not checked at runtime, so the zero guard in isFractional() stays.
  • Foundation check: not run, because this machine does not have FactoryKit.
  • Emulator check (earlier revision of this PR): a test app reads a live precompute response. The response serves all three flags as "number". Later changes do not affect these values.
Value Read 3.14.1 This PR
200 Int, Double 200, 200 200, 200
1 Int, Double 1, 1 1, 1
1.2 Int PARSE_ERROR TYPE_MISMATCH
1.2 Double 1.2 1.2

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)

@datadog-official

datadog-official Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Pipelines  Tests

🎉 All green!

🧪 All tests passed
❄️ No new flaky tests detected

🎯 Code Coverage (details)
• Patch Coverage: 97.06%
• Overall Coverage: 72.05% (-0.08%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 7e5ab28 | Docs | View more details | Give us feedback!

@aarsilv aarsilv added the crucible: reviewing A Crucible review has started; this PR has reviewer attention. label Nov 3, 2026
Read any numeric flag as Int when it is a whole number within Int range.
Return TYPE_MISMATCH for fractional or out-of-range values, keep
PARSE_ERROR for values that can't be parsed, and keep Double reads.

Ref: FFL-3565
@aarsilv
aarsilv force-pushed the aarsilv/ffl-3565/numeric-type-mismatch branch from 3a64335 to 6c1181d Compare November 3, 2026 21:12
@aarsilv aarsilv changed the title FFL-3565: Return TYPE_MISMATCH for mismatched numeric flag reads FFL-3565: Make Int reads of numeric flags lossless Nov 3, 2026
Cap decimal parsing at 64 characters, add non-numeric, boundary and
single-fit tests, and correct the README and KDoc. Unparseable values of
every numeric type, including "float", return PARSE_ERROR.

Ref: FFL-3565
Add 64- and 65-character boundary rows for the Int parse length limit,
describe the limit precisely in the README and resolveIntValue KDoc, and
reword the zero-scale comment to match observed Android behavior.

Ref: FFL-3565
@aarsilv aarsilv removed the crucible: reviewing A Crucible review has started; this PR has reviewer attention. label Nov 4, 2026
@aarsilv
aarsilv requested a balanced review from Copilot November 4, 2026 01:42

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The boundary tests lack a regression case for long zero-padded integers, leaving the documented length-check exemption unprotected.

1 open issue
What changed in this PR

This PR makes numeric flag reads as Int lossless across "integer", "number" and "float" values, while leaving Double reads unchanged.

Changes:

  • Accept whole numbers within Int range; distinguish type mismatches from parsing errors.
  • Bound decimal parsing to 64 characters, with an exception for plain integers within range.
  • Document the behavior and extend conversion, logging and telemetry tests.
File Description
reliability/​single-fit/​flags/​src/​test/​kotlin/​com/​datadog/​android/​flags/​integration/​FlagsEvaluationTest.kt Adds cross-type numeric resolution checks.
reliability/​single-fit/​flags/​src/​test/​kotlin/​com/​datadog/​android/​flags/​integration/​EvaluationTelemetryTest.kt Checks numeric type-mismatch telemetry.
features/​dd-sdk-android-flags/​src/​test/​kotlin/​com/​datadog/​android/​flags/​internal/​DatadogFlagsClientTest.kt Covers conversion boundaries, errors and warnings.
features/​dd-sdk-android-flags/​src/​main/​kotlin/​com/​datadog/​android/​flags/​internal/​FlagValueConverter.kt Implements bounded, lossless numeric-to-Int conversion.
features/​dd-sdk-android-flags/​src/​main/​kotlin/​com/​datadog/​android/​flags/​FlagsClient.kt Documents Int resolution and length limits.
features/​dd-sdk-android-flags/​README.md Explains numeric reads and error classifications.
detekt_custom_unsafe_calls.yml Registers the potentially throwing trailing-zero operation.
detekt_custom_safe_calls_third_party.yml Registers safe numeric parsing and conversion calls.

Review effort: Balanced

Add a 65-character zero-padded integer row so moving the length check
before toIntOrNull() fails a test.

Ref: FFL-3565

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

There are no active review issues for the changes aligning Int reads across numeric flag types.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

@aarsilv
aarsilv marked this pull request as ready for review November 4, 2026 03:10
@aarsilv
aarsilv requested review from a team as code owners November 4, 2026 03:10
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-04T12:14:07.975708Z 61f8293 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@aarsilv aarsilv added the ready-for-human-review All agentic review feedback was addressed and PR is ready for the human review. label Nov 4, 2026

@datadog-official datadog-official 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.

Bits Code Review: PASS

More details

No actionable conversion defect was reported, including in the intentional length-limit exemption for zero-padded plain integers. Gradle suites and Android runtime checks were not rerun.

Was this helpful? React 👍 or 👎

Open Bits AI session

🤖 Bits Code Review · Commit c90d1c3

Double::class ->
variationType == VariationType.NUMBER.value || variationType == VariationType.FLOAT.value ||
variationType == VariationType.INTEGER.value
Int::class, Double::class -> isNumeric(variationType)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

🤖 Context from Claude: Int reads now accept all three numeric types. A "number" value can hold a whole or a fractional number. OpenFeature defines one Number type. So the served type no longer decides an Int read. The parsed value does: a whole value in Int range succeeds. Double reads already accepted all three types.

Boolean::class -> variationValue.lowercase(Locale.US).toBooleanStrictOrNull() as? T
String::class -> variationValue as T
Int::class -> variationValue.toIntOrNull() as? T
Int::class -> parseLosslessInt(variationValue, variationType) as? T

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

🤖 Context from Claude: The Int conversion now runs inside runCatching. If the decimal path throws an unexpected exception, the client returns the default value with PARSE_ERROR. The exception does not reach the host app.

* [MAX_DECIMAL_LENGTH] characters.
*/
private fun parseLosslessInt(variationValue: String, variationType: String): Int? = variationValue.toIntOrNull()
?: variationValue.takeIf { it.length <= MAX_DECIMAL_LENGTH }

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

🤖 Context from Claude: Plain integers take the fast toIntOrNull() path. Other values longer than 64 characters skip the BigDecimal parse and report PARSE_ERROR. Without this limit, one long number string can take seconds on each read. On the JVM, a value of 1 followed by 200,000 zeros and .0 took about 10 s in stripTrailingZeros().

}
if (problem != null) {
@Suppress("ThrowingInternalException") // Caught by runCatching in convert()
throw TypeMismatchException("Flag has type '$variationType' with $problem but Int was requested")

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

🤖 Context from Claude: Parsing succeeded before this branch. A fractional value, or a whole value outside Int range, cannot be an Int without loss. So it is a type mismatch, not a parse error. runCatching in convert() catches the exception. The client then reports TYPE_MISMATCH.

* [BigDecimal.stripTrailingZeros].
*/
private fun isFractional(decimal: BigDecimal): Boolean {
if (decimal.scale() <= 0 || decimal.signum() == 0) return false

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

🤖 Context from Claude: BigDecimal tells whole values from fractional ones exactly, with no rounding. The scale check comes first: stripTrailingZeros() throws ArithmeticException on scale overflow for input such as 100e2147483647. The zero check is for older Android libcore, which returns a zero such as 0.0 unchanged from stripTrailingZeros(). The detekt config now lists stripTrailingZeros() as a throwing call.

"number, 5.00000000000000000000000000000000000000000000000000000000000000, 5",
"number, 00000000000000000000000000000000000000000000000000000000000000005, 5"
)
fun `M return value W resolve() { numeric flag, whole value, Int default }`(

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

🤖 Context from Claude: These tests protect lossless Int conversion for all numeric flag types. After parsing succeeds, a whole value in Int range succeeds. A parsed fractional or out-of-range value returns TYPE_MISMATCH. A value that cannot be parsed returns PARSE_ERROR. If direct Int parsing fails, an input over 64 characters also returns PARSE_ERROR. The -2147483649 row catches a removed lower bound. The -2147483648.0 row catches <= instead of <. The 100e2147483647 row catches a removed scale check. The 67-character row catches a removed length limit.

Arguments.of("enabled", "fallback", ErrorCode.TYPE_MISMATCH, optedOut),
Arguments.of("malformed", false, ErrorCode.PARSE_ERROR, optedOut)
Arguments.of("malformed", false, ErrorCode.PARSE_ERROR, optedOut),
Arguments.of("number", 0, ErrorCode.TYPE_MISMATCH, optedOut)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

🤖 Context from Claude: This end-to-end row reads the fixture's "number" value 2.5 as Int through the SDK's JSON response handling. It checks the default value, TYPE_MISMATCH, the evaluation count and that the SDK sends no exposure. With the previous converter, the read reports PARSE_ERROR, so this row fails.

Comment thread features/dd-sdk-android-flags/README.md Outdated
val discountPercentage = client.resolveDoubleValue("discount-rate", 0.0)
```

Any numeric flag can be read with either method. `resolveIntValue` returns the value only when it is a whole number within `Int` range, such as `5` or `5.0`. Otherwise it returns the default value: `resolve()` reports `TYPE_MISMATCH` when the value is fractional, such as `1.2`, or outside `Int` range, such as `2147483648`, and `PARSE_ERROR` when the value can't be parsed. To bound parsing work, a value longer than 64 characters is not parsed unless it is a plain integer within `Int` range, so it reports `PARSE_ERROR`.

@aarsilv aarsilv Nov 4, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

🤖 Context from Claude: This paragraph documents the Int-read and Double-read rules for app developers. The README definition of TYPE_MISMATCH, and the resolveIntValue and resolveDoubleValue KDoc, match it.

@aarsilv aarsilv removed the ready-for-human-review All agentic review feedback was addressed and PR is ready for the human review. label Nov 4, 2026
@aleksandr-gringauz

Copy link
Copy Markdown
Collaborator

Question - is it possible to use BigDecimal.intValueExact from the java stdlib?

}
if (problem != null) {
@Suppress("ThrowingInternalException") // Caught by runCatching in convert()
throw TypeMismatchException("Flag has type '$variationType' with $problem but Int was requested")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

worth to mark with method with @throws annotation to no forget to wrap this method in a future changes. Then suppressing wouldn't be needed

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

🤖 Reply from Claude, working with Aaron:

Good idea. 7e5ab28 adds @Throws(TypeMismatchException::class) to parseLosslessInt, requireLosslessInt, parseLosslessDouble and requireLosslessDouble, as FlagsJson does.

The @Suppress has to stay. The ThrowingInternalException rule reports every throw in internal code and does not read @Throws (tools/detekt/src/main/kotlin/com/datadog/tools/detekt/rules/sdk/ThrowingInternalException.kt). We checked: with @Throws and without the suppression, detekt reports both throw lines.

Return TYPE_MISMATCH for a whole value at 2^53 or more that matches
neither the Double nor its printed form. Return PARSE_ERROR for NaN,
Infinity and text over 64 characters at that magnitude. Update docs.

Ref: FFL-3565
@aarsilv aarsilv changed the title FFL-3565: Make Int reads of numeric flags lossless FFL-3565: Accept every numeric type in Int and Double reads Nov 4, 2026
* a value whose parsed Double is 2^53 or more in magnitude, when the value is longer than [MAX_DECIMAL_LENGTH]
* characters or [BigDecimal] can't parse it.
*/
private fun parseLosslessDouble(variationValue: String, variationType: String): Double? =

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

🤖 Context from Claude: This makes Double reads match the shared fixtures for large whole values. 9007199254740993 now gives TYPE_MISMATCH instead of a rounded value. Below 2^53 in magnitude, Double holds every whole value exactly, so the parsed value returns at once. At 2^53 or more, an input of at most 64 characters goes to BigDecimal. A longer input gives PARSE_ERROR. A whole value passes if it equals the Double or its printed form (see isExactDouble).

}

@Suppress("UnsafeThirdPartyFunctionCall") // Safe: BigDecimal(Double) and valueOf() only run for a finite Double
private fun isExactDouble(decimal: BigDecimal, double: Double): Boolean =

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

🤖 Context from Claude: A whole value also passes when the Double prints as that value, such as 1e300. So a double written in its shortest printed form reads back as itself. Without this check, a test that reads random doubles fails.

"integer, 9007199254740993",
"float, 9007199254740993"
)
fun `M return default with TYPE_MISMATCH W resolve() { whole value Double can't hold, Double default }`(

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

🤖 Context from Claude: These rows detect regressions in Double rejection. They cover shared fixture cases and edge cases such as 1e400. All 8 rows fail without the exactness check.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 61f82932ac

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Int::class -> variationValue.toIntOrNull() as? T
Double::class -> variationValue.toDoubleOrNull() as? T
Int::class -> parseLosslessInt(variationValue, variationType) as? T
Double::class -> parseLosslessDouble(variationValue, variationType) as? T

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve existing Double resolution behavior

When an existing numeric flag contains a value such as 9007199254740993, this change makes a Double read return the application default with TYPE_MISMATCH; NaN, Infinity, and sufficiently long values similarly change to errors. The parent implementation returned every value accepted by toDoubleOrNull(), so routing Double reads through parseLosslessDouble() silently changes public resolution and exposure behavior even though this change is scoped to lossless Int reads. Keep the direct Double conversion here, or move this separately as an explicit behavior change.

AGENTS.md reference: AGENTS.md:L270-L273

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

🤖 Reply from Claude, working with Aaron:

This change is intentional. The PR title and description now cover Double reads. The shared mobile fixtures in DataDog/ffe-system-test-data#65 expect TYPE_MISMATCH for a Double read of 9007199254740993, because the lossless rule does not allow a rounded result. The README and PR description document NaN, Infinity and the 64-character limit. The resolveDoubleValue KDoc describes the conversion rules and parse limits.

Comment thread features/dd-sdk-android-flags/README.md Outdated
val discountPercentage = client.resolveDoubleValue("discount-rate", 0.0)
```

Any numeric flag can be read with either method. `resolveIntValue` returns the value only when it is a whole number within `Int` range, such as `5` or `5.0`. Otherwise it returns the default value: `resolve()` reports `TYPE_MISMATCH` when the value is fractional, such as `1.2`, or outside `Int` range, such as `2147483648`, and `PARSE_ERROR` when the value can't be parsed. To bound parsing work, a value longer than 64 characters is not parsed unless it is a plain integer within `Int` range, so it reports `PARSE_ERROR`. `resolveDoubleValue` returns a fractional value as the nearest `Double`, such as `1.2`. It returns a whole value only when the `Double` has exactly that value, or prints as that value, such as `1e300`. Otherwise `resolve()` reports `TYPE_MISMATCH`, for example for `9007199254740993`. `NaN` and `Infinity` report `PARSE_ERROR`. So does a value longer than 64 characters whose `Double` is 2^53 or more in magnitude.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

🤖 Context from Claude: This paragraph documents the Int-read and Double-read rules for app developers. The README definition of TYPE_MISMATCH, and the resolveIntValue and resolveDoubleValue KDoc, match it.

@maycmlee maycmlee left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

small suggestions

Comment thread features/dd-sdk-android-flags/README.md Resolved
Comment thread features/dd-sdk-android-flags/README.md Resolved
Co-authored-by: May Lee <may.lee@datadoghq.com>
maycmlee
maycmlee previously approved these changes Nov 4, 2026
Add @throws to the lossless Int and Double helpers, note why Int reads
do not use intValueExact(), and fix two README slips from the applied
suggestions.

Ref: FFL-3565
@aarsilv

aarsilv commented Nov 4, 2026

Copy link
Copy Markdown
Author

🤖 Reply from Claude, working with Aaron:

is it possible to use BigDecimal.intValueExact from the java stdlib?

It is possible, but it is slow on older Android, so this PR keeps the explicit checks. intValueExact() rejected these inputs quickly on JVM 21 and Android 16. Some inputs took much longer on Android 9. We compared it with a Java copy of this PR's fraction and range checks. Both paths used an already parsed BigDecimal:

Input PR checks, all three runtimes intValueExact(), JVM 21 and Android 16 intValueExact(), Android 9 (API 28) emulator
1.2, 2147483648 < 0.05 ms < 0.05 ms < 0.05 ms
1e1000000 < 0.05 ms < 0.05 ms 287 ms
1e10000000 < 0.05 ms < 0.05 ms 20 s
1e99999999 < 0.05 ms < 0.05 ms did not finish in 120 s

On Android 9, intValueExact() expands the power of ten before it checks the range. Each of these inputs is shorter than the 64-character limit, and MIN_SDK is 23. The explicit checks also tell a fractional value from an out-of-range value, and the TYPE_MISMATCH message uses that. 7e5ab28 adds a comment on requireLosslessInt that records this.

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.

5 participants