From c3652f2c3a322107b85a9419732c79782f4da4fe Mon Sep 17 00:00:00 2001 From: Franco Zalamena Date: Thu, 13 Aug 2026 10:23:04 +0100 Subject: [PATCH 1/4] [SDK-568] Add stable cross-SDK identifiers to IterableDataRegion Phase 1 of the cross-platform data region parity effort (iOS SDK-609, RN SDK-610, Flutter SDK-611). Android already exposed a typed enum, so this hardens it into the shared reference shape rather than redesigning it. All changes are additive. Source compatibility was verified by compiling the pre-existing public call patterns (getEndpoint, valueOf, values, ordinal, enum switch, the deprecated constants) against the new classes. - Add getRegionCode() and getCode() as stable cross-SDK identifiers. Numeric codes are declared explicitly rather than derived from ordinal(), so adding a region cannot renumber the existing ones. - Add from(String) and from(int) factories for wrapper bridges. from(String) also accepts a full endpoint URL, so the iOS SDK's string-based region values resolve without translation. - Log and fall back to US on unrecognised, empty or null input instead of resolving silently, so a misconfigured region surfaces in the logs rather than quietly routing EU-destined data to the US data center. - setDataRegion(null) falls back to US with a warning instead of leaving the region unset. - Deprecate IterableConstants.BASE_URL_API and BASE_URL_LINKS: both are hardcoded to the US region and unused by the SDK, which resolves its endpoint from the configured region. - Drop a duplicated overrideUrl application in IterableRequestTask; getBaseUrl() already applies it. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 8 ++ .../iterable/iterableapi/IterableConfig.java | 11 +++ .../iterableapi/IterableConstants.java | 9 ++ .../iterableapi/IterableDataRegion.java | 99 ++++++++++++++++++- .../iterableapi/IterableRequestTask.java | 3 - .../iterableapi/IterableConfigTest.kt | 10 ++ .../iterableapi/IterableDataRegionTest.kt | 81 +++++++++++++++ 7 files changed, 215 insertions(+), 6 deletions(-) create mode 100644 iterableapi/src/test/java/com/iterable/iterableapi/IterableDataRegionTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d40ce8f5..9acd95517 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] +### Added +- `IterableDataRegion` now exposes stable cross-SDK identifiers and lookup helpers: `getRegionCode()` (e.g. `"EU"`), `getCode()` (`0` for US, `1` for EU, matching the React Native and Flutter SDKs), and the static `IterableDataRegion.from(String)` / `IterableDataRegion.from(int)` factories. `from(String)` accepts a region code in any case and also accepts a full API endpoint URL, so the iOS SDK's string-based data region values resolve without translation. **No action required** — the existing `setDataRegion(IterableDataRegion.EU)` API is unchanged. + +### Fixed +- An unrecognised data region no longer resolves silently. `IterableDataRegion.from(...)` and `setDataRegion(...)` log a warning naming the supported values before falling back to `US`, so a misconfigured region surfaces in the logs instead of quietly routing EU-destined data to the US data center. `setDataRegion(null)` now falls back to `US` with a warning instead of leaving the region unset. + +### Deprecated +- `IterableConstants.BASE_URL_API` and `IterableConstants.BASE_URL_LINKS` — both are hardcoded to the US data region and are unused by the SDK, which resolves its endpoint from the configured `IterableDataRegion`. Use `IterableDataRegion.getEndpoint()` instead. They still resolve to the same values, so no action is required; a removal version will be announced before they are removed. ## [3.10.0] ### Added diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java index d9e6b2542..a2f3fdd2c 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java @@ -362,10 +362,21 @@ public Builder setAllowedProtocols(@NonNull String[] allowedProtocols) { /** * Set the data region used by the SDK + *

+ * To resolve a region from a string or numeric identifier (for example when bridging from a + * cross-platform wrapper), use {@link IterableDataRegion#from(String)} or + * {@link IterableDataRegion#from(int)}, which fall back to + * {@link IterableDataRegion#US} and log on unrecognised values. + * * @param dataRegion enum value that determines which endpoint to use, defaults to IterableDataRegion.US */ @NonNull public Builder setDataRegion(@NonNull IterableDataRegion dataRegion) { + if (dataRegion == null) { + IterableLogger.w("IterableConfig", "setDataRegion received null, defaulting to " + IterableDataRegion.US.getRegionCode()); + this.dataRegion = IterableDataRegion.US; + return this; + } this.dataRegion = dataRegion; return this; } diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableConstants.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableConstants.java index 6c896f6d7..0c276930e 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableConstants.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableConstants.java @@ -11,7 +11,16 @@ public final class IterableConstants { public static final String ACTION_PUSH_REGISTRATION = "com.iterable.push.ACTION_PUSH_REGISTRATION"; //Hosts + /** + * @deprecated These constants are hardcoded to the US data region and are unused by the SDK. + * Use {@link IterableDataRegion#getEndpoint()} instead, which respects the configured region. + */ + @Deprecated public static final String BASE_URL_API = "https://api.iterable.com/api/"; + /** + * @deprecated Unused by the SDK and hardcoded to the US data region. + */ + @Deprecated public static final String BASE_URL_LINKS = "https://links.iterable.com/"; //API Fields diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableDataRegion.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableDataRegion.java index aec57c0f2..5fb051265 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableDataRegion.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableDataRegion.java @@ -1,16 +1,109 @@ package com.iterable.iterableapi; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * Data region determining which data center and endpoints the SDK sends data to. + * Defaults to {@link #US}; EU-hosted projects must select {@link #EU}. + */ public enum IterableDataRegion { - US("https://api.iterable.com/api/"), - EU("https://api.eu.iterable.com/api/"); + US(0, "US", "https://api.iterable.com/api/"), + EU(1, "EU", "https://api.eu.iterable.com/api/"); + + private static final String TAG = "IterableDataRegion"; + private final int code; + private final String regionCode; private final String endpoint; - IterableDataRegion(String endpoint) { + IterableDataRegion(int code, String regionCode, String endpoint) { + this.code = code; + this.regionCode = regionCode; this.endpoint = endpoint; } public String getEndpoint() { return this.endpoint; } + + /** + * Stable numeric identifier for this region, matching the values used by the React Native and + * Flutter SDKs. Unlike {@link #ordinal()} this is guaranteed not to shift if regions are added. + */ + public int getCode() { + return this.code; + } + + /** + * Stable short identifier for this region (for example {@code "EU"}), matching the values used + * by Iterable's other SDKs. + */ + @NonNull + public String getRegionCode() { + return this.regionCode; + } + + /** + * Resolves a region from its short identifier (e.g. {@code "EU"}), accepting either case. Also + * accepts a full API endpoint URL, so values from the iOS SDK's string-based data region can be + * passed through unchanged. + *

+ * Unrecognised or null values fall back to {@link #US} and are logged, matching the behaviour of + * Iterable's other SDKs. + * + * @param value region identifier, or {@code null} + * @return the matching region, or {@link #US} if the value is not recognised + */ + @NonNull + public static IterableDataRegion from(@Nullable String value) { + if (value == null || value.trim().isEmpty()) { + IterableLogger.w(TAG, "No data region specified, defaulting to " + US.regionCode); + return US; + } + + String normalized = value.trim(); + for (IterableDataRegion region : values()) { + if (normalized.equalsIgnoreCase(region.regionCode) || normalized.equalsIgnoreCase(region.endpoint)) { + return region; + } + } + + IterableLogger.w(TAG, "Unsupported data region \"" + value + "\", defaulting to " + US.regionCode + + ". Supported values: " + supportedRegionCodes()); + return US; + } + + /** + * Resolves a region from its numeric identifier, as used by the React Native and Flutter SDKs. + *

+ * Unrecognised values fall back to {@link #US} and are logged, matching the behaviour of + * Iterable's other SDKs. + * + * @param code region identifier, see {@link #getCode()} + * @return the matching region, or {@link #US} if the code is not recognised + */ + @NonNull + public static IterableDataRegion from(int code) { + for (IterableDataRegion region : values()) { + if (region.code == code) { + return region; + } + } + + IterableLogger.w(TAG, "Unsupported data region code " + code + ", defaulting to " + US.regionCode + + ". Supported values: " + supportedRegionCodes()); + return US; + } + + private static String supportedRegionCodes() { + StringBuilder builder = new StringBuilder(); + for (IterableDataRegion region : values()) { + if (builder.length() > 0) { + builder.append(", "); + } + builder.append(region.regionCode).append(" (").append(region.code).append(")"); + } + return builder.toString(); + } } diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableRequestTask.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableRequestTask.java index 33ecddd3f..d92f5825b 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableRequestTask.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableRequestTask.java @@ -87,9 +87,6 @@ static IterableApiResponse executeApiRequest(IterableApiRequest iterableApiReque String baseUrl = getBaseUrl(); try { - if (overrideUrl != null && !overrideUrl.isEmpty()) { - baseUrl = overrideUrl; - } if (iterableApiRequest.requestType == IterableApiRequest.GET) { Uri.Builder builder = Uri.parse(baseUrl + iterableApiRequest.resourcePath).buildUpon(); diff --git a/iterableapi/src/test/java/com/iterable/iterableapi/IterableConfigTest.kt b/iterableapi/src/test/java/com/iterable/iterableapi/IterableConfigTest.kt index b017c66a6..2ac9803f5 100644 --- a/iterableapi/src/test/java/com/iterable/iterableapi/IterableConfigTest.kt +++ b/iterableapi/src/test/java/com/iterable/iterableapi/IterableConfigTest.kt @@ -21,6 +21,16 @@ class IterableConfigTest { val config: IterableConfig = configBuilder.build() assertThat(config.dataRegion, `is`(IterableDataRegion.EU)) } + + /** Only reachable from Java, where the `@NonNull` parameter can still be passed null. */ + @Test + fun nullDataRegionFallsBackToUs() { + val builder = IterableConfig.Builder() + val setter = IterableConfig.Builder::class.java + .getMethod("setDataRegion", IterableDataRegion::class.java) + setter.invoke(builder, null) + assertThat(builder.build().dataRegion, `is`(IterableDataRegion.US)) + } @Test fun defaultWebViewBaseUrl() { diff --git a/iterableapi/src/test/java/com/iterable/iterableapi/IterableDataRegionTest.kt b/iterableapi/src/test/java/com/iterable/iterableapi/IterableDataRegionTest.kt new file mode 100644 index 000000000..03499b0b7 --- /dev/null +++ b/iterableapi/src/test/java/com/iterable/iterableapi/IterableDataRegionTest.kt @@ -0,0 +1,81 @@ +package com.iterable.iterableapi + +import org.hamcrest.Matchers.`is` +import org.junit.Assert.assertEquals +import org.junit.Assert.assertThat +import org.junit.Test + +class IterableDataRegionTest { + + @Test + fun endpointsMatchDataCenters() { + assertEquals("https://api.iterable.com/api/", IterableDataRegion.US.endpoint) + assertEquals("https://api.eu.iterable.com/api/", IterableDataRegion.EU.endpoint) + } + + /** These identifiers are part of the cross-SDK contract; changing them breaks the wrappers. */ + @Test + fun stableIdentifiersMatchOtherSdks() { + assertEquals(0, IterableDataRegion.US.code) + assertEquals(1, IterableDataRegion.EU.code) + assertEquals("US", IterableDataRegion.US.regionCode) + assertEquals("EU", IterableDataRegion.EU.regionCode) + } + + @Test + fun fromRegionCode() { + assertThat(IterableDataRegion.from("US"), `is`(IterableDataRegion.US)) + assertThat(IterableDataRegion.from("EU"), `is`(IterableDataRegion.EU)) + } + + @Test + fun fromRegionCodeIsCaseAndWhitespaceInsensitive() { + assertThat(IterableDataRegion.from("eu"), `is`(IterableDataRegion.EU)) + assertThat(IterableDataRegion.from("Eu"), `is`(IterableDataRegion.EU)) + assertThat(IterableDataRegion.from(" EU "), `is`(IterableDataRegion.EU)) + } + + /** iOS expresses the data region as the endpoint URL itself, so those values must resolve. */ + @Test + fun fromIosStyleEndpointUrl() { + assertThat( + IterableDataRegion.from("https://api.eu.iterable.com/api/"), + `is`(IterableDataRegion.EU) + ) + assertThat( + IterableDataRegion.from("https://api.iterable.com/api/"), + `is`(IterableDataRegion.US) + ) + } + + @Test + fun fromUnsupportedStringFallsBackToUs() { + assertThat(IterableDataRegion.from("APAC"), `is`(IterableDataRegion.US)) + assertThat(IterableDataRegion.from(""), `is`(IterableDataRegion.US)) + assertThat(IterableDataRegion.from(" "), `is`(IterableDataRegion.US)) + assertThat(IterableDataRegion.from(null as String?), `is`(IterableDataRegion.US)) + } + + @Test + fun fromCode() { + assertThat(IterableDataRegion.from(0), `is`(IterableDataRegion.US)) + assertThat(IterableDataRegion.from(1), `is`(IterableDataRegion.EU)) + } + + @Test + fun fromUnsupportedCodeFallsBackToUs() { + assertThat(IterableDataRegion.from(2), `is`(IterableDataRegion.US)) + assertThat(IterableDataRegion.from(-1), `is`(IterableDataRegion.US)) + assertThat(IterableDataRegion.from(Int.MAX_VALUE), `is`(IterableDataRegion.US)) + } + + /** Round-trips guard the wrappers, which serialize the region and resolve it back. */ + @Test + fun identifiersRoundTrip() { + for (region in IterableDataRegion.values()) { + assertThat(IterableDataRegion.from(region.code), `is`(region)) + assertThat(IterableDataRegion.from(region.regionCode), `is`(region)) + assertThat(IterableDataRegion.from(region.endpoint), `is`(region)) + } + } +} From d4c9288a570bbbccd6e06f13a9545a3d25aa564e Mon Sep 17 00:00:00 2001 From: Franco Zalamena Date: Thu, 13 Aug 2026 10:49:45 +0100 Subject: [PATCH 2/4] [SDK-568] Name 3.12.0 as the removal version for the deprecated BASE_URL constants An unnamed removal target is how BASE_URL_API survived as dead public API in the first place, so commit to a version clients can plan against. --- CHANGELOG.md | 2 +- .../main/java/com/iterable/iterableapi/IterableConstants.java | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9acd95517..208ae1215 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). - An unrecognised data region no longer resolves silently. `IterableDataRegion.from(...)` and `setDataRegion(...)` log a warning naming the supported values before falling back to `US`, so a misconfigured region surfaces in the logs instead of quietly routing EU-destined data to the US data center. `setDataRegion(null)` now falls back to `US` with a warning instead of leaving the region unset. ### Deprecated -- `IterableConstants.BASE_URL_API` and `IterableConstants.BASE_URL_LINKS` — both are hardcoded to the US data region and are unused by the SDK, which resolves its endpoint from the configured `IterableDataRegion`. Use `IterableDataRegion.getEndpoint()` instead. They still resolve to the same values, so no action is required; a removal version will be announced before they are removed. +- `IterableConstants.BASE_URL_API` and `IterableConstants.BASE_URL_LINKS` — both are hardcoded to the US data region and are unused by the SDK, which resolves its endpoint from the configured `IterableDataRegion`. Use `IterableDataRegion.getEndpoint()` instead. They still resolve to the same values, so no action is required in this release. **They will be removed in 3.12.0** — if you reference either constant, switch to `IterableDataRegion.getEndpoint()` before upgrading to that version. ## [3.10.0] ### Added diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableConstants.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableConstants.java index 0c276930e..403c12a05 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableConstants.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableConstants.java @@ -14,11 +14,13 @@ public final class IterableConstants { /** * @deprecated These constants are hardcoded to the US data region and are unused by the SDK. * Use {@link IterableDataRegion#getEndpoint()} instead, which respects the configured region. + * Scheduled for removal in 3.12.0. */ @Deprecated public static final String BASE_URL_API = "https://api.iterable.com/api/"; /** * @deprecated Unused by the SDK and hardcoded to the US data region. + * Scheduled for removal in 3.12.0. */ @Deprecated public static final String BASE_URL_LINKS = "https://links.iterable.com/"; From 0de07ac7efc59132add7b67e4adaab81b7063ee5 Mon Sep 17 00:00:00 2001 From: Franco Zalamena Date: Mon, 24 Aug 2026 13:43:39 +0100 Subject: [PATCH 3/4] [SDK-568] Document null data region fallback and why regionCode is stored Co-Authored-By: Claude Opus 5 --- .../java/com/iterable/iterableapi/IterableConfig.java | 8 +++++++- .../java/com/iterable/iterableapi/IterableDataRegion.java | 4 +++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java index a2f3fdd2c..8f3cf6d57 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java @@ -363,12 +363,18 @@ public Builder setAllowedProtocols(@NonNull String[] allowedProtocols) { /** * Set the data region used by the SDK *

+ * The parameter is annotated {@link NonNull}, but Java callers are not held to that at + * compile time: passing {@code null} falls back to {@link IterableDataRegion#US} and logs a + * warning rather than throwing. + *

* To resolve a region from a string or numeric identifier (for example when bridging from a * cross-platform wrapper), use {@link IterableDataRegion#from(String)} or * {@link IterableDataRegion#from(int)}, which fall back to * {@link IterableDataRegion#US} and log on unrecognised values. * - * @param dataRegion enum value that determines which endpoint to use, defaults to IterableDataRegion.US + * @param dataRegion enum value that determines which endpoint to use, defaults to + * IterableDataRegion.US; {@code null} is treated as + * {@link IterableDataRegion#US} */ @NonNull public Builder setDataRegion(@NonNull IterableDataRegion dataRegion) { diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableDataRegion.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableDataRegion.java index 5fb051265..917307043 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableDataRegion.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableDataRegion.java @@ -37,7 +37,9 @@ public int getCode() { /** * Stable short identifier for this region (for example {@code "EU"}), matching the values used - * by Iterable's other SDKs. + * by Iterable's other SDKs. It currently matches {@link #name()}, but is stored separately for + * the same reason {@link #getCode()} does not use {@link #ordinal()}: renaming an enum constant + * must not change an identifier that wrappers and persisted configuration already rely on. */ @NonNull public String getRegionCode() { From 01cf132aaaeb1aa02702a310ce79a19b21b6474e Mon Sep 17 00:00:00 2001 From: Franco Zalamena Date: Thu, 27 Aug 2026 09:44:54 +0100 Subject: [PATCH 4/4] [SDK-568] Address review comments Log unrecognised data regions at error level so they survive the default log level, and use TAG in setDataRegion. Cover both with tests asserting the message actually reaches logcat. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 2 +- .../iterable/iterableapi/IterableConfig.java | 6 +-- .../iterableapi/IterableDataRegion.java | 15 +++--- .../iterableapi/IterableConfigTest.kt | 19 +++++++ .../iterableapi/IterableDataRegionTest.kt | 49 +++++++++++++++++++ 5 files changed, 81 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d365e5d7c..de2ba1639 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Fixed - Fixed the keychain treating a transient crypto timeout as a permanent decryption failure. A slow AndroidKeyStore operation that exceeded the 500 ms timeout would wipe the stored email, userId, and auth token and disable encryption, forcing the user to re-authenticate (and request a new auth token) on the next launch. Crypto timeouts are now handled as transient without wiping credentials or disabling encryption for the device: a read that times out returns no value for that call (the stored ciphertext is left intact for the next attempt), and a write that times out stores that one value unencrypted (as the non-encrypted fallback already did) rather than clearing everything. The timed-out crypto operation is also cancelled so it no longer blocks subsequent reads/writes. - `setExpiringAuthTokenRefreshPeriod` now validates its input instead of silently producing a broken refresh schedule. Previously a negative value was converted to a negative millisecond period and then *subtracted* when computing the refresh time, scheduling the refresh after the token had already expired; a very large value overflowed to a negative period with the same effect; and `null` threw a `NullPointerException` on unboxing. Invalid values (`null`, `NaN`, negatives) are now logged and ignored, leaving the period at whatever it was before the call — the 60 second default unless an earlier call set something else. Values above ~10 years are clamped to that ceiling rather than ignored. Zero remains valid and means the token is refreshed only once it has expired. -- An unrecognised data region no longer resolves silently. `IterableDataRegion.from(...)` and `setDataRegion(...)` log a warning naming the supported values before falling back to `US`, so a misconfigured region surfaces in the logs instead of quietly routing EU-destined data to the US data center. `setDataRegion(null)` now falls back to `US` with a warning instead of leaving the region unset. +- An unrecognised data region no longer resolves silently. `IterableDataRegion.from(...)` and `setDataRegion(...)` log an error naming the supported values before falling back to `US`, so a misconfigured region surfaces in the logs instead of quietly routing EU-destined data to the US data center. `setDataRegion(null)` now falls back to `US` with an error instead of leaving the region unset. Error level is deliberate: these run while the config is still being built, before `IterableApi.initialize()` applies your `setLogLevel(...)`, so anything lower would never reach logcat. ### Changed - Clarified that `setExpiringAuthTokenRefreshPeriod` takes **seconds**, with a default of 60. The unit and default are unchanged and match every other Iterable SDK. diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java index 671b5faa4..67e037b21 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java @@ -422,8 +422,8 @@ public Builder setAllowedProtocols(@NonNull String[] allowedProtocols) { * Set the data region used by the SDK *

* The parameter is annotated {@link NonNull}, but Java callers are not held to that at - * compile time: passing {@code null} falls back to {@link IterableDataRegion#US} and logs a - * warning rather than throwing. + * compile time: passing {@code null} falls back to {@link IterableDataRegion#US} and logs an + * error rather than throwing. *

* To resolve a region from a string or numeric identifier (for example when bridging from a * cross-platform wrapper), use {@link IterableDataRegion#from(String)} or @@ -437,7 +437,7 @@ public Builder setAllowedProtocols(@NonNull String[] allowedProtocols) { @NonNull public Builder setDataRegion(@NonNull IterableDataRegion dataRegion) { if (dataRegion == null) { - IterableLogger.w("IterableConfig", "setDataRegion received null, defaulting to " + IterableDataRegion.US.getRegionCode()); + IterableLogger.e(TAG, "setDataRegion received null, defaulting to " + IterableDataRegion.US.getRegionCode()); this.dataRegion = IterableDataRegion.US; return this; } diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableDataRegion.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableDataRegion.java index 917307043..8175741a3 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableDataRegion.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableDataRegion.java @@ -51,8 +51,9 @@ public String getRegionCode() { * accepts a full API endpoint URL, so values from the iOS SDK's string-based data region can be * passed through unchanged. *

- * Unrecognised or null values fall back to {@link #US} and are logged, matching the behaviour of - * Iterable's other SDKs. + * Unrecognised values fall back to {@link #US} and are logged as an error, matching the + * behaviour of Iterable's other SDKs. A null or blank value also falls back to {@link #US}, + * which is the documented default rather than a misconfiguration. * * @param value region identifier, or {@code null} * @return the matching region, or {@link #US} if the value is not recognised @@ -71,7 +72,9 @@ public static IterableDataRegion from(@Nullable String value) { } } - IterableLogger.w(TAG, "Unsupported data region \"" + value + "\", defaulting to " + US.regionCode + // Error level, not warning: this runs while the config is still being built, and + // IterableLogger reads the pre-init config (ERROR) until initialize() swaps it in. + IterableLogger.e(TAG, "Unsupported data region \"" + value + "\", defaulting to " + US.regionCode + ". Supported values: " + supportedRegionCodes()); return US; } @@ -79,8 +82,8 @@ public static IterableDataRegion from(@Nullable String value) { /** * Resolves a region from its numeric identifier, as used by the React Native and Flutter SDKs. *

- * Unrecognised values fall back to {@link #US} and are logged, matching the behaviour of - * Iterable's other SDKs. + * Unrecognised values fall back to {@link #US} and are logged as an error, matching the + * behaviour of Iterable's other SDKs. * * @param code region identifier, see {@link #getCode()} * @return the matching region, or {@link #US} if the code is not recognised @@ -93,7 +96,7 @@ public static IterableDataRegion from(int code) { } } - IterableLogger.w(TAG, "Unsupported data region code " + code + ", defaulting to " + US.regionCode + IterableLogger.e(TAG, "Unsupported data region code " + code + ", defaulting to " + US.regionCode + ". Supported values: " + supportedRegionCodes()); return US; } diff --git a/iterableapi/src/test/java/com/iterable/iterableapi/IterableConfigTest.kt b/iterableapi/src/test/java/com/iterable/iterableapi/IterableConfigTest.kt index 513244beb..84d2ecf57 100644 --- a/iterableapi/src/test/java/com/iterable/iterableapi/IterableConfigTest.kt +++ b/iterableapi/src/test/java/com/iterable/iterableapi/IterableConfigTest.kt @@ -1,12 +1,23 @@ package com.iterable.iterableapi +import android.util.Log +import com.iterable.iterableapi.unit.TestRunner import org.hamcrest.Matchers.`is` import org.hamcrest.Matchers.nullValue import org.junit.Assert.* +import org.junit.Before import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.shadows.ShadowLog +@RunWith(TestRunner::class) class IterableConfigTest { + @Before + fun clearLogs() { + ShadowLog.clear() + } + @Test fun defaultDataRegion() { val configBuilder: IterableConfig.Builder = IterableConfig.Builder() @@ -30,6 +41,14 @@ class IterableConfigTest { .getMethod("setDataRegion", IterableDataRegion::class.java) setter.invoke(builder, null) assertThat(builder.build().dataRegion, `is`(IterableDataRegion.US)) + + // Builder methods run before initialize() swaps in this config, so IterableLogger is still + // reading the pre-init ERROR level: anything below ERROR never reaches logcat. + val errors = ShadowLog.getLogsForTag("IterableConfig") + .filter { it.type == Log.ERROR } + .map { it.msg } + assertEquals(errors.toString(), 1, errors.size) + assertTrue(errors.single(), errors.single().contains("setDataRegion received null")) } @Test diff --git a/iterableapi/src/test/java/com/iterable/iterableapi/IterableDataRegionTest.kt b/iterableapi/src/test/java/com/iterable/iterableapi/IterableDataRegionTest.kt index 03499b0b7..255b75dfb 100644 --- a/iterableapi/src/test/java/com/iterable/iterableapi/IterableDataRegionTest.kt +++ b/iterableapi/src/test/java/com/iterable/iterableapi/IterableDataRegionTest.kt @@ -1,12 +1,24 @@ package com.iterable.iterableapi +import android.util.Log +import com.iterable.iterableapi.unit.TestRunner import org.hamcrest.Matchers.`is` import org.junit.Assert.assertEquals import org.junit.Assert.assertThat +import org.junit.Assert.assertTrue +import org.junit.Before import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.shadows.ShadowLog +@RunWith(TestRunner::class) class IterableDataRegionTest { + @Before + fun clearLogs() { + ShadowLog.clear() + } + @Test fun endpointsMatchDataCenters() { assertEquals("https://api.iterable.com/api/", IterableDataRegion.US.endpoint) @@ -78,4 +90,41 @@ class IterableDataRegionTest { assertThat(IterableDataRegion.from(region.endpoint), `is`(region)) } } + + /** + * These factories run while the config is still being built, so IterableLogger is still reading + * the pre-init config's ERROR level. Anything below ERROR is dropped before it reaches logcat, + * which would leave a misconfigured region silently sending data to the US data center. + */ + @Test + fun unsupportedStringIsLoggedAtDefaultLogLevel() { + IterableDataRegion.from("APAC") + + val message = errorLogs().single() + assertTrue("log should name the rejected value: $message", message.contains("APAC")) + assertTrue("log should list supported values: $message", message.contains("US (0), EU (1)")) + } + + @Test + fun unsupportedCodeIsLoggedAtDefaultLogLevel() { + IterableDataRegion.from(7) + + val message = errorLogs().single() + assertTrue("log should name the rejected code: $message", message.contains("7")) + assertTrue("log should list supported values: $message", message.contains("US (0), EU (1)")) + } + + @Test + fun recognisedValuesAreNotLoggedAsErrors() { + IterableDataRegion.from("EU") + IterableDataRegion.from("https://api.eu.iterable.com/api/") + IterableDataRegion.from(0) + + assertTrue(errorLogs().toString(), errorLogs().isEmpty()) + } + + private fun errorLogs(): List = + ShadowLog.getLogsForTag("IterableDataRegion") + .filter { it.type == Log.ERROR } + .map { it.msg } }