Skip to content

Feat/eit/sdk 4341 #798

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 60 commits into
base: develop
Choose a base branch
from
Open

Feat/eit/sdk 4341 #798

wants to merge 60 commits into from

Conversation

CTLalit
Copy link
Collaborator

@CTLalit CTLalit commented Apr 30, 2025

https://wizrocket.atlassian.net/browse/SDK-4341

Summary by CodeRabbit

  • New Features

    • Added support for encryption in transit configurable via app manifest and settings.
    • Introduced new network and encryption management components including key generation, encryption/decryption, and request metadata handling.
    • Added persistent storage management for network request timestamps, domains, and mute state.
    • Added new network queue header builder for enriched request metadata.
    • Introduced new Kotlin-based network manager replacing the previous Java implementation.
    • Added utility extensions for Base64 encoding/decoding.
  • Improvements

    • Refactored network and encryption logic for enhanced modularity and maintainability.
    • Improved request header construction with detailed metadata and ARP integration.
    • Enhanced response processing with expanded support for CleverTap features.
    • Updated push notification ID handling to use string arrays and JSON conversion.
    • Replaced direct Android dependencies in encryption classes with injected key generators.
  • Bug Fixes

    • Updated push notification ID handling and ARP data processing for increased reliability.
  • Tests

    • Added comprehensive unit tests for encryption, network management, persistent storage, and header building.
    • Migrated mocking framework to MockK for improved test reliability.
    • Added new tests for IJRepo and NetworkRepo persistent storage classes.
    • Extended network manager tests covering encryption, response handling, and error cases.
  • Chores

    • Increased JVM heap size for unit tests to enhance stability.
  • Style

    • Minor formatting and code style refinements.

Copy link
Contributor

coderabbitai bot commented Apr 30, 2025

## Walkthrough

This update introduces a new encryption-in-transit feature and a major refactor of the network layer in the CleverTap Android SDK. It adds new classes for key management, encryption, ARP/IJ storage, and network state, and replaces the legacy `NetworkManager.java` with a new Kotlin implementation. Multiple classes are updated to support these changes, and comprehensive unit tests are added.

## Changes

| File(s) / Group                                                                                 | Change Summary |
|-------------------------------------------------------------------------------------------------|---------------|
| `CleverTapFactory.kt`, `DBManager.kt`, `CleverTapAPI.java`, `SessionManager.java`               | Refactored core initialization and session logic to support new encryption and network components; updated method calls and dependencies. |
| `CleverTapInstanceConfig.java`, `ManifestInfo.java`, `CoreMetaData.java`                        | Added encryption-in-transit configuration fields and accessors; updated JSON and parcel serialization; added relaxNetwork flag. |
| `Constants.java`                                                                                | Removed obsolete header and namespace constants. |
| `cryption/AESGCMCrypt.kt`, `cryption/CTKeyGenerator.kt`, `cryption/CryptFactory.kt`,<br>`cryption/CryptRepository.kt`, `cryption/CryptExtensions.kt` | Refactored cryptography classes to use injected key generator and repository; removed direct Android key management from crypt classes; added Base64 utilities. |
| `network/ArpRepo.kt`, `network/IJRepo.kt`, `network/NetworkEncryptionManager.kt`,<br>`network/NetworkManager.kt`, `network/NetworkRepo.kt`, `network/QueueHeaderBuilder.kt` | Introduced new Kotlin classes for ARP/IJ storage, network encryption, network state, and header building; replaced legacy Java network manager. |
| `network/api/CtApi.kt`, `network/api/CtApiWrapper.kt`, `network/api/EncryptedResponseBody.kt`,<br>`network/api/EncryptedSendQueueRequestBody.kt`, `network/api/EncryptionResult.kt` | Updated network API to support encryption, new header/query constants, and new request/response body structures; added encryption result types. |
| `response/ARPResponse.java`, `response/MetadataResponse.java`, `response/PushAmpResponse.java`,<br>`response/CleverTapResponseDecorator.java` | Refactored response classes to use new ARP/IJ repositories and updated method signatures; minor formatting and logic updates. |
| `utils/CTJsonConverter.java`                                                                    | Replaced DB-dependent push ID conversion with a direct method. |
| `sample/src/main/AndroidManifest.xml`, `sample/src/main/java/com/clevertap/demo/MyApplication.kt` | Added encryption-in-transit manifest flag and demo in-app button listener. |
| `gradle-scripts/commons.gradle`                                                                 | Increased JVM heap for unit tests. |
| `test/java/com/clevertap/android/sdk/`, `test/java/com/clevertap/android/sdk/network/`,<br>`test/java/com/clevertap/android/sdk/network/api/`, `test/java/com/clevertap/android/sdk/network/http/` | Added/updated tests for new network, encryption, ARP/IJ, and header builder classes; migrated to MockK; expanded test coverage; removed legacy dependencies. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant App
    participant CleverTapFactory
    participant CTKeyGenerator
    participant CryptFactory
    participant NetworkEncryptionManager
    participant NetworkManager
    participant NetworkRepo
    participant ArpRepo
    participant IJRepo
    participant QueueHeaderBuilder
    participant CtApi

    App->>CleverTapFactory: Initialize CleverTap
    CleverTapFactory->>CTKeyGenerator: Create/generate key
    CleverTapFactory->>CryptFactory: Inject CTKeyGenerator
    CleverTapFactory->>NetworkEncryptionManager: Inject CTKeyGenerator, CryptFactory
    CleverTapFactory->>IJRepo: Create IJRepo
    CleverTapFactory->>ArpRepo: Create ArpRepo
    CleverTapFactory->>NetworkRepo: Create NetworkRepo
    CleverTapFactory->>QueueHeaderBuilder: Inject dependencies
    CleverTapFactory->>NetworkManager: Inject encryptionManager, arpRepo, ijRepo, networkRepo, queueHeaderBuilder

    App->>NetworkManager: flushDBQueue / sendQueue
    NetworkManager->>QueueHeaderBuilder: buildHeader()
    NetworkManager->>CtApi: sendQueue(headers, body, isEncrypted)
    CtApi-->>NetworkManager: Response (possibly encrypted)
    NetworkManager->>NetworkEncryptionManager: decryptResponse (if encrypted)
    NetworkEncryptionManager-->>NetworkManager: Decrypted payload or failure
    NetworkManager->>ResponseProcessors: processResponse()

Possibly related PRs

  • CleverTap/clevertap-android-sdk#797: The main PR introduces extensive internal refactoring and new components related to network encryption, repository abstractions, and in-app messaging system templates, including the injection of an InAppActionHandler and system templates into the CleverTapFactory, while the retrieved PR (Release Core 7.4.0 #797) primarily focuses on releasing version 7.4.0 with system in-app templates and advanced in-app builder features; thus, the main PR’s changes to CleverTapFactory and in-app template management are directly related to the system in-app templates feature highlighted in the retrieved PR.

Suggested labels

ptv2.0.0

Suggested reviewers

  • piyush-kukadiya
  • Anush-Shand


<!-- walkthrough_end -->


---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between b9ad941838dcac557e178040d7f8ea565f917037 and 5b4835b37ed41c9778f8467cb24be6848e2cc496.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `clevertap-core/src/test/java/com/clevertap/android/sdk/network/NetworkManagerTest.kt` (6 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧠 Learnings (1)</summary>

<details>
<summary>📓 Common learnings</summary>

Learnt from: CTLalit
PR: #798
File: clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt:152-152
Timestamp: 2025-05-30T08:21:16.962Z
Learning: CTLalit prefers to avoid modifying legacy code when working on specific features, keeping the scope focused to reduce risk and maintain clear boundaries for changes.


</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (12)</summary><blockquote>

<details>
<summary>clevertap-core/src/test/java/com/clevertap/android/sdk/network/NetworkManagerTest.kt (12)</summary>

`9-9`: **Import addition looks appropriate**

The `TestLogger` import aligns with its usage in the `provideNetworkManager` method at line 615.

---

`15-18`: **Good practice: Using header constants instead of hardcoded strings**

Replacing hardcoded header strings with constants from `CtApi.Companion` improves maintainability and reduces the risk of typos.

---

`21-22`: **Encryption-related imports support new test functionality**

The `EncryptionFailure` and `EncryptionSuccess` imports are used appropriately in the encryption test methods.

---

`83-83`: **Excellent refactor: Using header constants**

The replacement of hardcoded strings (`"X-WZRK-MUTE"`, `"X-WZRK-RD"`, etc.) with constants from `CtApi.Companion` improves maintainability and consistency across the codebase.



Also applies to: 91-91, 104-105

---

`129-144`: **Well-structured test with proper verification**

The updated test correctly verifies that when response body is null, the `sendQueue` method returns true and ensures that neither decryption nor response processing occurs. The MockK verification syntax is properly used.

---

`162-198`: **Comprehensive domain change test with clear AAA pattern**

This test effectively validates the behavior when domain changes are detected in response headers. The use of MockK's `every` and `verify` for `networkRepo` interactions is appropriate, and the test covers both event groups.

---

`200-247`: **Thorough mute header processing tests**

These tests comprehensively cover various mute header scenarios:
- `true` value causing `sendQueue` to return false
- `false` value allowing normal processing 
- Garbage values being handled gracefully

The verification of `networkRepo.setMuted()` calls ensures proper state management.

---

`249-311`: **Request timestamp management tests are well-designed**

These tests effectively validate the timestamp saving behavior:
- First-time requests save both first and last timestamps
- Subsequent requests only update the last timestamp

The use of MockK's `verify(exactly = 0)` provides precise assertion of method call counts.

---

`313-414`: **Event-specific response processing tests are comprehensive**

These tests thoroughly validate the `isFullResponse` flag behavior based on event types:
- App Launched and WZRK_FETCH events trigger full response processing
- Regular events use non-full response processing

The test data construction with proper event structures adds good coverage.

---

`416-526`: **Encryption functionality tests are thorough and well-structured**

These tests comprehensively cover encryption scenarios:
- Response decryption when encryption header is true
- Proper error handling when decryption fails  
- No decryption when header is absent or false

The use of `EncryptionSuccess` and `EncryptionFailure` sealed classes provides type-safe testing.

---

`528-554`: **HTTP error code tests provide good edge case coverage**

The tests for HTTP 402 and 419 error codes ensure proper error handling for these specific status codes that may have special meaning in the CleverTap context.

---

`587-587`: **Updated NetworkManager constructor aligns with new architecture**

The addition of new dependencies (`IJRepo`, `QueueHeaderBuilder`, `networkEncryptionManager`, `ARPResponse`, `networkRepo`, `cleverTapResponses`) reflects the comprehensive refactoring to support encryption and improved network state management. The mock configuration is appropriate for testing.



Also applies to: 590-616

</details>

</blockquote></details>

</details>
<!-- internal state start -->


<!-- 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= -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=CleverTap/clevertap-android-sdk&utm_content=798):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->

@CTLalit CTLalit changed the base branch from release/core_7.4.0 to develop May 12, 2025 05:44
CTLalit added 23 commits May 12, 2025 11:15
- exposes the encryption in transit flag in config.
- Extracts out the CTKeyGenerator class which is responsible for key generation
- Moves extensions to correct class
- adds methods to crypt repo for key persistence
- Make data classes for request and response bodies
- introduces NetworkEncryptionManager which is abstraction of feature encrypt/decrypt in network layer of SDK.
- passes correct params to refactored classes.
- adds check for encryption on network and relevant helper classes
- adds param in CtApi so that is can be passed in the header
- exposes session level enc key
- fixes test cases
- adds check for encryption on network and relevant helper classes
- adds param in CtApi so that is can be passed in the header
- exposes session level enc key
- fixes test cases
- introduces constants in the network manager class for header fields.
- decrypts the response received at SDK if required
- header value is read to check if response is encrypted
- the keys used for parsing were incorrect.
- failed due to using Mockito mocking with Mockk verify methods.
- fixes incorrect json string which was failing the tests.
- handles the prefs edits for the respective things in their own repo.
- breaks down dependencies
- lighter and more testable NetworkManager.kt
- fixes duplication in testing code
- bubbles up logger for better test case logging
- fixes test for 200ok case with null body.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (6)
clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt (6)

111-119: mNetworkHeadersListeners is not thread-safe – replace with CopyOnWriteArrayList.

addNetworkHeadersListener() / removeNetworkHeadersListener() may be called from the main thread while callbacks are iterated on a worker thread. Using a plain ArrayList risks ConcurrentModificationException.


830-832: Consider logging decryption failure details for debugging.

When decryption fails, the method returns false without logging the failure reason, which could make troubleshooting difficult.


893-894: Improve exception handling for JSON parsing.

The JSONException is silently swallowed with only a comment, which could hide important parsing errors.

🧰 Tools
🪛 detekt (1.23.8)

[warning] 893-893: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


921-928: Improve exception handling in push notification processing.

Multiple exception types are caught but not properly logged, making debugging difficult. The e.printStackTrace() should be replaced with proper logging.

🧰 Tools
🪛 detekt (1.23.8)

[warning] 921-921: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


293-453: Break down the getQueueHeader method to improve maintainability.

This 160-line method violates the Single Responsibility Principle by handling device info, app fields, identities, notifications, and referrer data all in one place.


1019-1031: Modernize network connectivity check and improve error handling.

The current implementation uses deprecated activeNetworkInfo API and the optimistic fallback approach might mask real connectivity issues.

🧹 Nitpick comments (1)
clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt (1)

699-706: Document encryption fallback behavior for security transparency.

The method silently falls back to unencrypted requests when encryption fails (lines 699-706). For security-sensitive applications, this behavior should be clearly documented and potentially made configurable.

Consider adding debug logging when fallback occurs:

} else {
+   logger.verbose(config.accountId, "Encryption failed, falling back to unencrypted request: ${encryptionResult}")
    logger.verbose("Normal Request cause encryption failed = $body")
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f7c4e8d and 75efd63.

📒 Files selected for processing (2)
  • clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt (1 hunks)
  • clevertap-core/src/main/java/com/clevertap/android/sdk/network/api/CtApi.kt (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • clevertap-core/src/main/java/com/clevertap/android/sdk/network/api/CtApi.kt
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: CTLalit
PR: CleverTap/clevertap-android-sdk#798
File: clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt:152-152
Timestamp: 2025-05-30T08:21:16.938Z
Learning: CTLalit prefers to avoid modifying legacy code when working on specific features, keeping the scope focused to reduce risk and maintain clear boundaries for changes.
clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt (1)
Learnt from: CTLalit
PR: CleverTap/clevertap-android-sdk#798
File: clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt:558-558
Timestamp: 2025-05-30T08:41:45.282Z
Learning: In the CleverTap Android SDK, debug logging that includes request bodies is controlled by build configuration and only enabled in development builds, not in production. This build-time control already addresses potential sensitive data exposure concerns.
🧬 Code Graph Analysis (1)
clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt (11)
clevertap-core/src/main/java/com/clevertap/android/sdk/response/ARPResponse.java (1)
  • ARPResponse (19-139)
clevertap-core/src/main/java/com/clevertap/android/sdk/response/ConsoleResponse.java (1)
  • ConsoleResponse (10-56)
clevertap-core/src/main/java/com/clevertap/android/sdk/response/InboxResponse.java (1)
  • InboxResponse (14-80)
clevertap-core/src/main/java/com/clevertap/android/sdk/response/PushAmpResponse.java (1)
  • PushAmpResponse (19-132)
clevertap-core/src/main/java/com/clevertap/android/sdk/response/FetchVariablesResponse.java (1)
  • FetchVariablesResponse (14-82)
clevertap-core/src/main/java/com/clevertap/android/sdk/response/DisplayUnitResponse.java (1)
  • DisplayUnitResponse (15-96)
clevertap-core/src/main/java/com/clevertap/android/sdk/response/FeatureFlagResponse.java (1)
  • FeatureFlagResponse (12-69)
clevertap-core/src/main/java/com/clevertap/android/sdk/response/ProductConfigResponse.java (1)
  • ProductConfigResponse (13-87)
clevertap-core/src/main/java/com/clevertap/android/sdk/network/api/CtApi.kt (2)
  • sendQueue (66-82)
  • performHandshakeForDomain (84-104)
clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkHeadersListener.kt (1)
  • fromEventGroup (29-36)
clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/evaluation/EvaluationManager.kt (1)
  • fromBoolean (37-39)
🪛 detekt (1.23.8)
clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt

[warning] 152-152: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 893-893: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 921-921: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (4)
clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt (4)

111-119: mNetworkHeadersListeners is not thread-safe – replace with CopyOnWriteArrayList.

addNetworkHeadersListener() / removeNetworkHeadersListener() may be called from the main thread while callbacks are iterated on a worker thread (notifyNetworkHeaderListeners). Using a plain ArrayList risks ConcurrentModificationException.

-    private val mNetworkHeadersListeners: MutableList<NetworkHeadersListener> = ArrayList()
+    private val mNetworkHeadersListeners = CopyOnWriteArrayList<NetworkHeadersListener>()

885-886: 🛠️ Refactor suggestion

Improve exception handling for JSON parsing.

The JSONException is silently swallowed with only a comment, which could hide important parsing errors.

            } catch (jsonException: JSONException) {
-                //skip
+                logger.verbose(config.accountId, "Failed to parse event in queue", jsonException)
🧰 Tools
🪛 detekt (1.23.8)

[warning] 885-885: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


913-920: 🛠️ Refactor suggestion

Improve exception handling in push notification processing.

Multiple exception types are caught but not properly logged, making debugging difficult. The e.printStackTrace() should be replaced with proper logging.

            } catch (e: JSONException) {
-                logger.verbose(
-                    config.accountId,
-                    "Encountered an exception while parsing the push notification viewed event queue"
-                )
+                logger.verbose(
+                    config.accountId,
+                    "Exception while parsing push notification viewed event queue",
+                    e
+                )
            } catch (e: Exception) {
-                e.printStackTrace()
+                logger.verbose(
+                    config.accountId,
+                    "Unexpected exception while processing push notification queue",
+                    e
+                )
🧰 Tools
🪛 detekt (1.23.8)

[warning] 913-913: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


152-157: 🛠️ Refactor suggestion

Improve exception handling to preserve original exception context.

The exception is caught and swallowed without logging the original exception details, which could make debugging difficult.

                        } catch (e: Exception) {
-                            config.logger.verbose(
-                                config.accountId,
-                                "met with exception while notifying listeners for PushImpressionSentToServer event"
-                            )
+                            config.logger.verbose(
+                                config.accountId,
+                                "Exception while notifying listeners for PushImpressionSentToServer event",
+                                e
+                            )
🧰 Tools
🪛 detekt (1.23.8)

[warning] 152-152: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)

🧹 Nitpick comments (3)
clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt (3)

293-453: Break down the getQueueHeader method to improve maintainability.

This 160-line method violates the Single Responsibility Principle by handling device info, app fields, identities, notifications, and referrer data all in one place.

Consider decomposing into smaller, focused methods:

private fun getQueueHeader(context: Context, caller: String?): JSONObject? {
    return try {
        val header = JSONObject()
-        // Add caller if available
-        if (caller != null) {
-            header.put(Constants.D_SRC, caller)
-        }
-        // ... 150+ lines of header construction
+        
+        addBasicFields(header, caller)
+        addDeviceAndAppFields(header)
+        addIdentityFields(header)
+        addNotificationFields(header)
+        addReferrerFields(header)
+        addMetricsFields(header, context)
+        
        header
    } catch (e: JSONException) {
        logger.verbose(config.accountId, "Failed to attach header", e)
        null
    }
}

+private fun addBasicFields(header: JSONObject, caller: String?) { /* ... */ }
+private fun addDeviceAndAppFields(header: JSONObject) { /* ... */ }
+private fun addIdentityFields(header: JSONObject) { /* ... */ }
+private fun addNotificationFields(header: JSONObject) { /* ... */ }
+private fun addReferrerFields(header: JSONObject) { /* ... */ }
+private fun addMetricsFields(header: JSONObject, context: Context) { /* ... */ }

822-824: Consider logging encryption failures for debugging.

When decryption fails, the method returns false without logging the failure reason, which could make troubleshooting difficult.

                is EncryptionFailure -> {
+                    logger.verbose(config.accountId, "Failed to decrypt response: ${decryptResponse.error}")
                    return false // todo lp check if this should be considered as nw failure?
                }

1011-1023: Modernize network connectivity check and improve error handling.

The current implementation uses deprecated activeNetworkInfo API and the optimistic fallback approach might mask real connectivity issues.

Consider updating to use modern connectivity APIs:

@JvmStatic
fun isNetworkOnline(context: Context): Boolean {
    return try {
        val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager
            ?: return false // be more conservative when service unavailable
-        @SuppressLint("MissingPermission") val netInfo = cm.activeNetworkInfo
-        return netInfo != null && netInfo.isConnected
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            val network = cm.activeNetwork ?: return false
+            val capabilities = cm.getNetworkCapabilities(network) ?: return false
+            return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+        } else {
+            @SuppressLint("MissingPermission") 
+            val netInfo = cm.activeNetworkInfo
+            return netInfo != null && netInfo.isConnected
+        }
    } catch (e: SecurityException) {
+        logger.verbose(config.accountId, "ACCESS_NETWORK_STATE permission required", e)
+        return false // be conservative when permission missing
    } catch (ignore: Exception) {
-        // lets be optimistic, if we are truly offline we handle the exception
-        return true
+        logger.verbose(config.accountId, "Failed to check network connectivity", ignore)
+        return false // be conservative on unexpected errors
    }
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 75efd63 and d1b3900.

📒 Files selected for processing (1)
  • clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: CTLalit
PR: CleverTap/clevertap-android-sdk#798
File: clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt:152-152
Timestamp: 2025-05-30T08:21:16.938Z
Learning: CTLalit prefers to avoid modifying legacy code when working on specific features, keeping the scope focused to reduce risk and maintain clear boundaries for changes.
clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt (1)
Learnt from: CTLalit
PR: CleverTap/clevertap-android-sdk#798
File: clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt:558-558
Timestamp: 2025-05-30T08:41:45.282Z
Learning: In the CleverTap Android SDK, debug logging that includes request bodies is controlled by build configuration and only enabled in development builds, not in production. This build-time control already addresses potential sensitive data exposure concerns.
🧬 Code Graph Analysis (1)
clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt (10)
clevertap-core/src/main/java/com/clevertap/android/sdk/response/ARPResponse.java (1)
  • ARPResponse (19-139)
clevertap-core/src/main/java/com/clevertap/android/sdk/response/ConsoleResponse.java (1)
  • ConsoleResponse (10-56)
clevertap-core/src/main/java/com/clevertap/android/sdk/response/InboxResponse.java (1)
  • InboxResponse (14-80)
clevertap-core/src/main/java/com/clevertap/android/sdk/response/PushAmpResponse.java (1)
  • PushAmpResponse (19-132)
clevertap-core/src/main/java/com/clevertap/android/sdk/response/DisplayUnitResponse.java (1)
  • DisplayUnitResponse (15-96)
clevertap-core/src/main/java/com/clevertap/android/sdk/response/FeatureFlagResponse.java (1)
  • FeatureFlagResponse (12-69)
clevertap-core/src/main/java/com/clevertap/android/sdk/response/ProductConfigResponse.java (1)
  • ProductConfigResponse (13-87)
clevertap-core/src/main/java/com/clevertap/android/sdk/network/api/CtApi.kt (2)
  • sendQueue (66-82)
  • performHandshakeForDomain (84-104)
clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkHeadersListener.kt (1)
  • fromEventGroup (29-36)
clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/evaluation/EvaluationManager.kt (1)
  • fromBoolean (37-39)
🪛 detekt (1.23.8)
clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt

[warning] 152-152: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 885-885: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 913-913: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)

🔇 Additional comments (2)
clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt (2)

63-103: Well-structured constructor with proper dependency injection.

The constructor design effectively uses dependency injection and clearly separates concerns. The initialization of response processors in the cleverTapResponses list is comprehensive and properly structured.


63-1025: Excellent architectural design and Kotlin modernization.

This NetworkManager implementation successfully:

Modernizes the codebase: Clean Kotlin rewrite with proper language idioms and null safety
Integrates new features: Seamless integration with encryption (NetworkEncryptionManager), repositories (IJRepo, ArpRepo), and updated API components
Maintains extensibility: Well-designed response processing chain and proper dependency injection
Follows best practices: Clear separation of concerns and comprehensive error handling patterns

The class effectively replaces the legacy Java implementation while adding substantial new functionality like encryption in transit.

🧰 Tools
🪛 detekt (1.23.8)

[warning] 152-152: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 885-885: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 913-913: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)

CTLalit added 4 commits May 30, 2025 15:02
- handles json validation and correct json
- simplifies code slightly
…ers in a readable class, Code coverage for header object

- Introduces a new queue header builder class
- Fixes retry counter ignoring variables response success
- adds code coverage for header object which was missing
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (5)
clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt (3)

109-117: mNetworkHeadersListeners is not thread-safe – replace with CopyOnWriteArrayList.

addNetworkHeadersListener() / removeNetworkHeadersListener() may be called from the main thread while callbacks are iterated on a worker thread (notifyNetworkHeaderListeners). Using a plain ArrayList risks ConcurrentModificationException.

-    private val mNetworkHeadersListeners: MutableList<NetworkHeadersListener> = ArrayList()
+    private val mNetworkHeadersListeners = CopyOnWriteArrayList<NetworkHeadersListener>()

515-541: Add configurable encryption fallback and document behavior.

The current implementation silently falls back to unencrypted requests whenever encryptResponse returns EncryptionFailure. For security-sensitive use cases, this behavior should be both documented and made optional.

• Document the fallback in your public docs (e.g., README.md or SDK reference), clearly stating that on encryption failure the SDK will retry unencrypted.
• Expose a new flag in CleverTapInstanceConfig (for example, strictEncryptionMode) to disable the automatic fallback and instead surface an error or drop the request.
• Update NetworkManager to respect this flag and add tests covering both fallback-enabled and strict-mode scenarios.


656-668: Consider logging encryption failures for debugging.

When decryption fails, the method returns false without logging the failure reason, which could make troubleshooting difficult.

                is EncryptionFailure -> {
+                    logger.verbose(config.accountId, "Failed to decrypt response: ${decryptResponse.error}")
                    return false // todo lp check if this should be considered as nw failure?
                }
clevertap-core/src/test/java/com/clevertap/android/sdk/network/NetworkManagerTest.kt (2)

72-75: Reset MockK interactions in tearDown() to avoid cross-test leakage

The same networkEncryptionManager mock instance is shared across all tests, yet its recorded interactions are never cleared.
Tests that verify call counts (verify(exactly = 0) { … }) can therefore fail or give false-positives depending on previous test execution order.

 @After
 fun tearDown() {
     closeable?.close()
+    // Ensures mocks don't carry interaction history into the next test run
+    clearAllMocks()   // <-- import io.mockk.clearAllMocks
 }

536-540: Replace spyk with a relaxed mockk for CleverTapResponse

spyk<CleverTapResponse>() tries to create a spy of the real class, calling the original (often abstract / open) methods.
If CleverTapResponse is an interface or an abstract class with un-implemented members, the spy will throw at runtime when processResponse is invoked by sendQueue.

Switching to a relaxed mock avoids this risk while still letting you verify the interaction.

-for (i in 1..10) {
-    val spyProcessor = spyk<CleverTapResponse>()
-    responses.add(spyProcessor)
-}
+for (i in 1..10) {
+    val processor = mockk<CleverTapResponse>(relaxed = true)
+    responses.add(processor)
+}
🧹 Nitpick comments (6)
clevertap-core/src/main/java/com/clevertap/android/sdk/utils/CTJsonConverter.java (1)

181-188: Consider using consistent logging pattern.

The method uses Logger.v() directly while other methods in the class use injected ILogger instances passed as parameters. For consistency with the existing codebase patterns, consider either:

  1. Adding an ILogger parameter to this method, or
  2. Using a consistent logging approach across the utility class

The implementation logic is correct and handles the conversion properly.

clevertap-core/src/main/java/com/clevertap/android/sdk/network/QueueHeaderBuilder.kt (3)

15-28: Consider reducing constructor dependencies.

The constructor takes 11 dependencies, which suggests high coupling and potential violation of the Single Responsibility Principle. Consider:

  1. Grouping related dependencies into dedicated service classes
  2. Using a builder pattern for QueueHeaderBuilder itself
  3. Creating focused sub-builders for different header sections

This would improve testability and maintainability.


120-123: Document side effect in header building.

The method modifies coreMetaData.isBgPing as a side effect during header construction. Consider documenting this behavior or moving the state update to a separate method to maintain clearer separation of concerns.

+// Note: Resets background ping flag after including it in header
 if (coreMetaData.isBgPing) {
     header.put("bk", 1)
     coreMetaData.isBgPing = false
 }

139-141: Document side effect in session flag handling.

Similar to the background ping flag, this method has a side effect of resetting the first request session flag. Consider adding documentation or refactoring to separate state management from header building.

+// Note: Resets first request flag after including it in header
 header.put("frs", coreMetaData.isFirstRequestInSession)
 coreMetaData.isFirstRequestInSession = false
clevertap-core/src/test/java/com/clevertap/android/sdk/network/QueueHeaderBuilderTest.kt (2)

102-185: Consider splitting the comprehensive test.

While this test provides excellent coverage of all header fields, it's quite large (80+ lines). Consider splitting it into focused tests for different header sections (e.g., config fields, metadata fields, etc.) for better maintainability and clearer failure diagnostics.

The current implementation is thorough and correct, but smaller, focused tests would be easier to debug when they fail.


167-168: Verify identity set assertions.

The assertions for ct_pi field check for the presence of "Email" and "Identity" strings, but these seem like implementation details. Consider verifying the actual structure or format rather than string containment, as the identity set format might change.

-assertThat(header.optString("ct_pi")).contains("Email")
-assertThat(header.optString("ct_pi")).contains("Identity")
+// Consider verifying the actual JSON structure instead
+assertThat(header.optString("ct_pi")).isNotEmpty()
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2ae1b5b and 459526d.

📒 Files selected for processing (8)
  • clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapAPI.java (1 hunks)
  • clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapFactory.kt (6 hunks)
  • clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt (1 hunks)
  • clevertap-core/src/main/java/com/clevertap/android/sdk/network/QueueHeaderBuilder.kt (1 hunks)
  • clevertap-core/src/main/java/com/clevertap/android/sdk/response/PushAmpResponse.java (2 hunks)
  • clevertap-core/src/main/java/com/clevertap/android/sdk/utils/CTJsonConverter.java (1 hunks)
  • clevertap-core/src/test/java/com/clevertap/android/sdk/network/NetworkManagerTest.kt (8 hunks)
  • clevertap-core/src/test/java/com/clevertap/android/sdk/network/QueueHeaderBuilderTest.kt (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapFactory.kt
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: CTLalit
PR: CleverTap/clevertap-android-sdk#798
File: clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt:152-152
Timestamp: 2025-05-30T08:21:16.962Z
Learning: CTLalit prefers to avoid modifying legacy code when working on specific features, keeping the scope focused to reduce risk and maintain clear boundaries for changes.
clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt (1)
Learnt from: CTLalit
PR: CleverTap/clevertap-android-sdk#798
File: clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt:558-558
Timestamp: 2025-05-30T08:41:45.328Z
Learning: In the CleverTap Android SDK, debug logging that includes request bodies is controlled by build configuration and only enabled in development builds, not in production. This build-time control already addresses potential sensitive data exposure concerns.
🧬 Code Graph Analysis (2)
clevertap-core/src/main/java/com/clevertap/android/sdk/response/PushAmpResponse.java (1)
clevertap-core/src/main/java/com/clevertap/android/sdk/db/DBAdapter.kt (1)
  • fetchPushNotificationIds (113-137)
clevertap-core/src/test/java/com/clevertap/android/sdk/network/NetworkManagerTest.kt (2)
test_shared/src/main/java/com/clevertap/android/shared/test/BaseTestCase.kt (1)
  • getSampleJsonArrayOfJsonObjects (58-67)
clevertap-core/src/test/java/com/clevertap/android/sdk/network/IJRepoTest.kt (1)
  • assertFalse (654-656)
🪛 detekt (1.23.8)
clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt

[warning] 150-150: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 716-716: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 744-744: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)

🔇 Additional comments (18)
clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapAPI.java (1)

1188-1188: LGTM! Clean API update following NetworkManager refactoring.

The removal of the context parameter from the defineTemplates method call correctly aligns with the new Kotlin NetworkManager API. This change improves the API design by reducing parameter coupling and suggests the context is now handled internally by the NetworkManager implementation.

clevertap-core/src/main/java/com/clevertap/android/sdk/response/PushAmpResponse.java (2)

3-3: LGTM: Import statement updated correctly.

The import has been properly updated to use the new pushIdsToJSONArray method from CTJsonConverter.


79-80:

✅ Verification successful

Verify null handling for push notification IDs.

The change correctly uses the new fetchPushNotificationIds() method and converts the result using pushIdsToJSONArray. However, based on the relevant code snippet from DBAdapter.kt, the fetchPushNotificationIds() method returns Array<String?> (nullable strings), but the pushIdsToJSONArray method expects String[].

Please verify that null values are properly handled in this conversion. Run the following script to check the method signatures:


🏁 Script executed:

#!/bin/bash
# Description: Verify the method signatures for push ID handling
# Expected: fetchPushNotificationIds returns Array<String?>, pushIdsToJSONArray expects String[]

echo "Checking fetchPushNotificationIds signature:"
ast-grep --pattern 'fun fetchPushNotificationIds(): $RETURN_TYPE'

echo "Checking pushIdsToJSONArray signature:"
ast-grep --pattern 'public static JSONArray pushIdsToJSONArray($PARAM_TYPE)'

Length of output: 332


To pinpoint the exact signatures, let’s locate their definitions in the codebase:


🏁 Script executed:

#!/bin/bash
# Search for the Kotlin declaration of fetchPushNotificationIds
echo "=== fetchPushNotificationIds definition ==="
rg -n "fun fetchPushNotificationIds" -C3

# Search for the Java declaration of pushIdsToJSONArray
echo "=== pushIdsToJSONArray definition ==="
rg -n "pushIdsToJSONArray" -C3

Length of output: 4476


Null-safe handling of push IDs is confirmed
fetchPushNotificationIds() returns a non-null String[] (elements may be null), and pushIdsToJSONArray explicitly guards against null/empty values via Strings.isNullOrEmpty(pushId) before adding to the JSONArray. No further changes are needed.

clevertap-core/src/main/java/com/clevertap/android/sdk/network/QueueHeaderBuilder.kt (2)

29-60: LGTM: Well-structured header building orchestration.

The buildHeader method provides good error handling and clear orchestration of header field addition. The early return for missing credentials is appropriate.


184-189: Verify InAppFCManager null handling.

The null-safe call and logging are good, but ensure that the getInAppsCount(context) method can handle the context parameter safely and doesn't throw exceptions that could break header construction.

clevertap-core/src/test/java/com/clevertap/android/sdk/network/QueueHeaderBuilderTest.kt (4)

18-46: LGTM: Well-structured test setup with comprehensive mocking.

Good use of mockk for relaxed mocking and proper dependency injection setup for the builder instance.


47-55: LGTM: Clear test for caller inclusion.

The test properly verifies that the caller parameter is included in the header when provided.


57-65: LGTM: Proper static mocking for debug level.

Good use of mockStatic with proper resource management using use block to test the debug flag inclusion.


67-74: LGTM: Important edge case for null credentials.

This test correctly verifies that the builder returns null when essential configuration is missing, which prevents malformed requests.

clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt (4)

60-101: Well-designed dependency injection architecture.

The constructor properly injects all dependencies including the new encryption components (NetworkEncryptionManager, IJRepo, ArpRepo, QueueHeaderBuilder), making the class testable and following SOLID principles.


126-181: Solid queue flushing implementation with proper batch processing.

The flushDBQueue method correctly handles:

  • Batch processing with 50 items per batch
  • Proper cursor management for pagination
  • Push notification impression handling
  • Network error callbacks and batch listeners
🧰 Tools
🪛 detekt (1.23.8)

[warning] 150-150: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


288-290: Good refactoring – header construction now properly delegated.

Using QueueHeaderBuilder for header construction improves modularity and testability compared to the previous monolithic approach mentioned in past reviews.


350-362: Clean implementation of mute SDK functionality.

The shouldMuteSdk method properly handles the mute header with appropriate null safety and trimming, returning early when SDK should be muted.

clevertap-core/src/test/java/com/clevertap/android/sdk/network/NetworkManagerTest.kt (5)

160-194: Excellent test coverage for domain change scenarios.

The test_sendQueue_domainChanges_returnFalse test properly:

  • Sets up domain change conditions
  • Verifies the method returns false as expected
  • Checks that the domain was actually updated
  • Cleans up state for other tests

372-431: Comprehensive encryption test coverage.

The encryption-related test methods provide excellent coverage:

  • Testing successful decryption with proper header
  • Testing decryption failure scenarios
  • Testing cases where encryption header is absent or false
  • Proper verification of NetworkEncryptionManager interactions

269-297: Well-structured test for full response flag behavior.

The test correctly verifies that when special events (APP_LAUNCHED_EVENT, WZRK_FETCH) are present, all response processors receive the isFullResponse flag set to true.


506-563: Good dependency injection in test setup.

The provideNetworkManager method properly injects all new dependencies including IJRepo, ArpRepo, NetworkEncryptionManager, and QueueHeaderBuilder, matching the production constructor signature.


10-11: Good addition of test utilities.

The imports of StorageHelper and TestLogger support the enhanced test scenarios for storage manipulation and logging verification.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (11)
clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt (7)

69-77: Thread safety issue with listener collection.

The mNetworkHeadersListeners ArrayList is not thread-safe and can cause ConcurrentModificationException when listeners are added/removed from different threads while being iterated.

-    private val mNetworkHeadersListeners: MutableList<NetworkHeadersListener> = ArrayList()
+    private val mNetworkHeadersListeners = CopyOnWriteArrayList<NetworkHeadersListener>()

475-501: Encryption fallback behavior may need configuration option.

The current implementation silently falls back to unencrypted requests when encryption fails. For security-sensitive applications, this behavior should be configurable to either fail fast or continue unencrypted.

Consider adding a strictEncryptionMode configuration flag to control this behavior.


614-627: Add logging for decryption failures.

When decryption fails, the method returns false without logging the failure reason, making troubleshooting difficult.

                is EncryptionFailure -> {
+                    logger.verbose(config.accountId, "Failed to decrypt response: ${decryptResponse.error}")
                    return false
                }

675-677: Improve JSON exception handling.

The JSONException is silently ignored, which could hide important parsing errors that would aid debugging.

            } catch (jsonException: JSONException) {
-                //skip
+                logger.verbose(config.accountId, "Failed to parse event in queue", jsonException)
🧰 Tools
🪛 detekt (1.23.8)

[warning] 675-675: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


703-710: Improve exception handling in push notification processing.

Multiple exception types are caught but not properly logged. The e.printStackTrace() should be replaced with proper logging.

            } catch (e: JSONException) {
-                logger.verbose(
-                    config.accountId,
-                    "Encountered an exception while parsing the push notification viewed event queue"
-                )
+                logger.verbose(
+                    config.accountId,
+                    "Exception while parsing push notification viewed event queue",
+                    e
+                )
            } catch (e: Exception) {
-                e.printStackTrace()
+                logger.verbose(
+                    config.accountId,
+                    "Unexpected exception while processing push notification queue",
+                    e
+                )
🧰 Tools
🪛 detekt (1.23.8)

[warning] 703-703: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


801-813: Modernize network connectivity check.

The current implementation uses deprecated activeNetworkInfo API and the optimistic fallback approach might mask real connectivity issues.

Consider updating to use modern ConnectivityManager APIs for Android M+ and implement more conservative error handling.


69-77: ⚠️ Potential issue

mNetworkHeadersListeners is not thread-safe – replace with CopyOnWriteArrayList.

addNetworkHeadersListener() / removeNetworkHeadersListener() may be called from the main thread while callbacks are iterated on a worker thread (notifyNetworkHeaderListeners). Using a plain ArrayList risks ConcurrentModificationException.

-    private val mNetworkHeadersListeners: MutableList<NetworkHeadersListener> = ArrayList()
+    private val mNetworkHeadersListeners = CopyOnWriteArrayList<NetworkHeadersListener>()
clevertap-core/src/test/java/com/clevertap/android/sdk/network/NetworkManagerTest.kt (4)

68-71: Add MockK cleanup to prevent cross-test interference.

The tearDown method doesn't clear MockK interactions, which can cause test failures due to interaction history carrying over between tests.

 @After
 fun tearDown() {
     closeable?.close()
+    clearAllMocks()
 }

520-524: Replace spyk with relaxed mockk for CleverTapResponse.

Using spyk<CleverTapResponse>() may cause runtime issues if CleverTapResponse has abstract/unimplemented methods. A relaxed mock is safer for testing.

-for (i in 1..10) {
-    val spyProcessor = spyk<CleverTapResponse>()
-    responses.add(spyProcessor)
-}
+for (i in 1..10) {
+    val processor = mockk<CleverTapResponse>(relaxed = true)
+    responses.add(processor)
+}

68-71: ⚠️ Potential issue

Reset MockK interactions in tearDown() to avoid cross-test leakage.

The same networkEncryptionManager mock instance is shared across all tests, yet its recorded interactions are never cleared. Tests that verify call counts (verify(exactly = 0) { … }) can therefore fail or give false-positives depending on previous test execution order.

 @After
 fun tearDown() {
     closeable?.close()
+    // Ensures mocks don't carry interaction history into the next test run
+    clearAllMocks()   // <-- import io.mockk.clearAllMocks
 }

520-524: 🛠️ Refactor suggestion

Replace spyk with a relaxed mockk for CleverTapResponse.

spyk<CleverTapResponse>() tries to create a spy of the real class, calling the original (often abstract / open) methods. If CleverTapResponse is an interface or an abstract class with un-implemented members, the spy will throw at runtime when processResponse is invoked by sendQueue.

-for (i in 1..10) {
-    val spyProcessor = spyk<CleverTapResponse>()
-    responses.add(spyProcessor)
-}
+for (i in 1..10) {
+    val processor = mockk<CleverTapResponse>(relaxed = true)
+    responses.add(processor)
+}
🧹 Nitpick comments (1)
clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapFactory.kt (1)

308-345: Consider breaking down QueueHeaderBuilder instantiation to improve readability.

The QueueHeaderBuilder constructor has grown quite large with 12+ parameters including lambda functions. This impacts readability and maintainability.

Consider extracting the lambda functions into separate methods:

+    private fun createFirstRequestTsProvider() = {
+        StorageHelper.getIntFromPrefs(
+            context,
+            config,
+            Constants.KEY_FIRST_TS,
+            0
+        )
+    }
+    
+    private fun createLastRequestTsProvider() = {
+        StorageHelper.getIntFromPrefs(
+            context,
+            config,
+            Constants.KEY_LAST_TS,
+            0
+        )
+    }

     val queueHeaderBuilder = QueueHeaderBuilder(
         context = context,
         config = config,
         coreMetaData = coreMetaData,
         controllerManager = controllerManager,
         deviceInfo = deviceInfo,
         arpRepo = arpRepo,
         ijRepo = ijRepo,
         databaseManager = baseDatabaseManager,
         validationResultStack = validationResultStack,
-        firstRequestTs = {
-            StorageHelper.getIntFromPrefs(
-                context,
-                config,
-                Constants.KEY_FIRST_TS,
-                0
-            )
-        },
-        lastRequestTs = {
-            StorageHelper.getIntFromPrefs(
-                context,
-                config,
-                Constants.KEY_LAST_TS,
-                0
-            )
-        },
+        firstRequestTs = createFirstRequestTsProvider(),
+        lastRequestTs = createLastRequestTsProvider(),
         logger = config.logger
     )
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 459526d and d3d545f.

📒 Files selected for processing (4)
  • clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapFactory.kt (6 hunks)
  • clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt (1 hunks)
  • clevertap-core/src/main/java/com/clevertap/android/sdk/response/ARPResponse.java (4 hunks)
  • clevertap-core/src/test/java/com/clevertap/android/sdk/network/NetworkManagerTest.kt (7 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • clevertap-core/src/main/java/com/clevertap/android/sdk/response/ARPResponse.java
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: CTLalit
PR: CleverTap/clevertap-android-sdk#798
File: clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt:152-152
Timestamp: 2025-05-30T08:21:16.962Z
Learning: CTLalit prefers to avoid modifying legacy code when working on specific features, keeping the scope focused to reduce risk and maintain clear boundaries for changes.
clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt (1)
Learnt from: CTLalit
PR: CleverTap/clevertap-android-sdk#798
File: clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt:558-558
Timestamp: 2025-05-30T08:41:45.328Z
Learning: In the CleverTap Android SDK, debug logging that includes request bodies is controlled by build configuration and only enabled in development builds, not in production. This build-time control already addresses potential sensitive data exposure concerns.
🧬 Code Graph Analysis (1)
clevertap-core/src/test/java/com/clevertap/android/sdk/network/NetworkManagerTest.kt (3)
test_shared/src/main/java/com/clevertap/android/shared/test/BaseTestCase.kt (1)
  • getSampleJsonArrayOfJsonObjects (58-67)
clevertap-core/src/test/java/com/clevertap/android/sdk/network/IJRepoTest.kt (1)
  • assertFalse (654-656)
clevertap-core/src/main/java/com/clevertap/android/sdk/ControllerManager.java (1)
  • ControllerManager (23-243)
🪛 detekt (1.23.8)
clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt

[warning] 110-110: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 675-675: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 703-703: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)

🔇 Additional comments (13)
clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapFactory.kt (6)

112-123: Good architectural improvement with IJRepo integration.

The introduction of IJRepo for managing IJ data and its injection into DBManager improves separation of concerns and follows the repository pattern consistently implemented across the codebase.


130-134: Excellent security architecture improvement.

Centralizing cryptographic key management in CTKeyGenerator improves security by providing a dedicated, well-defined interface for key operations. This removes key management logic from AESGCMCrypt and aligns with security best practices.


308-345: NetworkEncryptionManager and QueueHeaderBuilder integration looks solid.

The NetworkEncryptionManager setup correctly integrates encryption capabilities. While QueueHeaderBuilder has many dependencies, this is necessary for comprehensive header construction and represents good dependency injection practices.


372-384: Well-structured NetworkManager constructor with named parameters.

The use of named parameters significantly improves readability and maintainability. The integration of new dependencies (encryptionManager, arpResponse, queueHeaderBuilder) and expanded cleverTapResponses list properly supports the new encryption and modularization features.


112-123: LGTM! Clean integration of IJRepo into the dependency chain.

The IJRepo is properly instantiated and passed to DBManager, establishing a clear separation of concerns for IJ data management.


348-370:

✅ Verification successful

Verify the expanded CleverTapResponse list includes all necessary processors.

The list has grown significantly with many new response types. Ensure that all required response processors are included and properly ordered if order matters.


🏁 Script executed:

#!/bin/bash
# Description: Verify all response types are properly implemented and used
# Expected: Find all CleverTapResponse implementations and their usage

# Search for all CleverTapResponse implementations
rg -A 3 "class.*Response.*implements|extends.*CleverTapResponse|: CleverTapResponse" --type kotlin --type java

# Search for any missing response processors that might be referenced elsewhere
rg "Response\(" --type kotlin --type java | grep -E "(process|handle)"

Length of output: 14902


CleverTapResponse list is complete and correctly ordered

All 11 concrete CleverTapResponse implementations are present in the cleverTapResponses list:

  • InAppResponse
  • MetadataResponse
  • ARPResponse
  • ConsoleResponse
  • InboxResponse
  • PushAmpResponse
  • FetchVariablesResponse
  • DisplayUnitResponse
  • FeatureFlagResponse
  • ProductConfigResponse
  • GeofenceResponse

No missing processors or ordering issues detected.

clevertap-core/src/test/java/com/clevertap/android/sdk/network/NetworkManagerTest.kt (4)

364-449: Excellent encryption test coverage.

The encryption-related tests comprehensively cover successful decryption, failed decryption, and non-encrypted scenarios. The use of MockK's every/verify patterns and exact call count verification demonstrates good testing practices.


152-190: Excellent test coverage for domain change scenarios.

This test thoroughly validates the domain change detection and handling logic, including proper state management and cleanup.


264-293: LGTM! Comprehensive test for isFullResponse flag behavior.

The test properly validates that response processors receive the correct isFullResponse flag when App Launched events are present, ensuring proper downstream processing.


368-398: Well-structured encryption test with proper mock verification.

The test effectively validates the encryption/decryption flow and verifies that decrypted data is properly passed to response processors.

clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt (3)

47-61: Constructor parameter count is high but well-organized with dependency injection.

The constructor has 10+ parameters which is complex but follows a clear dependency injection pattern. The named parameters make it readable and the dependencies are well-typed.


248-250: LGTM! Clean delegation to QueueHeaderBuilder.

The extraction of header building logic into QueueHeaderBuilder improves separation of concerns and testability.


614-627: LGTM! Clean encryption/decryption integration with proper error handling.

The encryption header detection and decryption logic is well-implemented with appropriate error handling for decryption failures.

- removes network shared prefs ops and adds them to a new NetworkRepo.kt class
- NetworkManager.kt and QueueHeaderBuilder.kt will access data from repo
- todo pending in other classes and move constants
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (5)
clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt (3)

558-568: Consider logging encryption failures for debugging.

When decryption fails, the method returns false without logging the failure reason, which could make troubleshooting difficult.

                is EncryptionFailure -> {
+                    logger.verbose(config.accountId, "Failed to decrypt response: ${decryptResponse.error}")
                    return false // todo lp check if this should be considered as nw failure?
                }

69-77: ⚠️ Potential issue

mNetworkHeadersListeners is not thread-safe – replace with CopyOnWriteArrayList.

addNetworkHeadersListener() / removeNetworkHeadersListener() may be called from the main thread while callbacks are iterated on a worker thread (notifyNetworkHeaderListeners). Using a plain ArrayList risks ConcurrentModificationException.

-    private val mNetworkHeadersListeners: MutableList<NetworkHeadersListener> = ArrayList()
+    private val mNetworkHeadersListeners = CopyOnWriteArrayList<NetworkHeadersListener>()

416-442: 🛠️ Refactor suggestion

Document encryption fallback behavior and consider making it configurable.

The implementation silently falls back to unencrypted requests when encryption fails (lines 433-435). This behavior should be documented and potentially made configurable for security-sensitive applications.

Consider adding a configuration flag to control fallback behavior:

private fun sendQueueApi(body: SendQueueRequestBody): Response {
    if (config.isEncryptionInTransitEnabled && coreMetaData.isRelaxNetwork.not()) {
        val encryptionResult = encryptionManager.encryptResponse(body.toString())
        val sessionEncryptionKey = encryptionManager.sessionEncryptionKey()

        if (encryptionResult is EncryptionSuccess) {
            // ... encrypted send logic
        } else {
            logger.verbose("Encryption failed, falling back to unencrypted request")
            if (config.strictEncryptionMode) {
                throw SecurityException("Encryption required but failed")
            }
        }
    }
    return ctApiWrapper.ctApi.sendQueue(
        isViewedEvent = false,
        body = body.toString(),
        isEncrypted = false
    )
}
clevertap-core/src/test/java/com/clevertap/android/sdk/network/NetworkManagerTest.kt (2)

68-71: 🛠️ Refactor suggestion

Reset MockK interactions in tearDown() to avoid cross-test leakage

The same networkEncryptionManager mock instance is shared across all tests, yet its recorded interactions are never cleared. Tests that verify call counts (verify(exactly = 0) { … }) can therefore fail or give false-positives depending on previous test execution order.

 @After
 fun tearDown() {
     closeable?.close()
+    // Ensures mocks don't carry interaction history into the next test run
+    clearAllMocks()   // <-- import io.mockk.clearAllMocks
 }

514-518: 🛠️ Refactor suggestion

Replace spyk with a relaxed mockk for CleverTapResponse

spyk<CleverTapResponse>() tries to create a spy of the real class, calling the original (often abstract / open) methods. If CleverTapResponse is an interface or an abstract class with un-implemented members, the spy will throw at runtime when processResponse is invoked by sendQueue.

Switching to a relaxed mock avoids this risk while still letting you verify the interaction.

-for (i in 1..10) {
-    val spyProcessor = spyk<CleverTapResponse>()
-    responses.add(spyProcessor)
-}
+for (i in 1..10) {
+    val processor = mockk<CleverTapResponse>(relaxed = true)
+    responses.add(processor)
+}
🧹 Nitpick comments (1)
clevertap-core/src/main/java/com/clevertap/android/sdk/network/http/NetworkRepo.kt (1)

81-87: Consider adding a getter method for spiky domain.

The class provides setSpikyDomain() but no corresponding getSpikyDomain() method, which breaks the typical getter/setter pattern established for other properties.

Add the missing getter method:

 fun setSpikyDomain(spikyDomainName: String) {
     StorageHelper.putString(
         context,
         StorageHelper.storageKeyWithSuffix(config.accountId, Constants.SPIKY_KEY_DOMAIN_NAME),
         spikyDomainName
     )
 }

+fun getSpikyDomain(): String? {
+    return StorageHelper.getStringFromPrefs(
+        context,
+        config,
+        Constants.SPIKY_KEY_DOMAIN_NAME,
+        null
+    )
+}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ae3e59e and bcb55a4.

📒 Files selected for processing (4)
  • clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapFactory.kt (6 hunks)
  • clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt (1 hunks)
  • clevertap-core/src/main/java/com/clevertap/android/sdk/network/http/NetworkRepo.kt (1 hunks)
  • clevertap-core/src/test/java/com/clevertap/android/sdk/network/NetworkManagerTest.kt (6 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapFactory.kt
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: CTLalit
PR: CleverTap/clevertap-android-sdk#798
File: clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt:152-152
Timestamp: 2025-05-30T08:21:16.962Z
Learning: CTLalit prefers to avoid modifying legacy code when working on specific features, keeping the scope focused to reduce risk and maintain clear boundaries for changes.
clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt (1)
Learnt from: CTLalit
PR: CleverTap/clevertap-android-sdk#798
File: clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt:558-558
Timestamp: 2025-05-30T08:41:45.328Z
Learning: In the CleverTap Android SDK, debug logging that includes request bodies is controlled by build configuration and only enabled in development builds, not in production. This build-time control already addresses potential sensitive data exposure concerns.
🧬 Code Graph Analysis (1)
clevertap-core/src/test/java/com/clevertap/android/sdk/network/NetworkManagerTest.kt (2)
test_shared/src/main/java/com/clevertap/android/shared/test/BaseTestCase.kt (1)
  • getSampleJsonArrayOfJsonObjects (58-67)
clevertap-core/src/main/java/com/clevertap/android/sdk/ControllerManager.java (1)
  • ControllerManager (23-243)
🪛 detekt (1.23.8)
clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt

[warning] 110-110: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 616-616: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 644-644: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)

🔇 Additional comments (2)
clevertap-core/src/test/java/com/clevertap/android/sdk/network/NetworkManagerTest.kt (2)

369-398: Excellent test coverage for encryption scenarios.

The new encryption-related test methods provide comprehensive coverage of encryption/decryption scenarios including success, failure, and header-based control flow. The test setup properly mocks the NetworkEncryptionManager and verifies both the encryption calls and the data flow to response processors.


264-327: Well-structured tests for response processing flags.

The tests effectively verify that the isFullResponse flag is correctly set based on the presence of APP_LAUNCHED_EVENT and WZRK_FETCH events in the queue. The verification of processor interactions ensures the contract between NetworkManager and CleverTapResponse implementations is maintained.

Comment on lines 14 to 28
fun getFirstRequestTs() : Int {
return StorageHelper.getIntFromPrefs(
context,
config,
Constants.KEY_FIRST_TS,
0
)
}
fun setFirstRequestTs(firstRequestTs: Int) {
StorageHelper.putInt(
context,
StorageHelper.storageKeyWithSuffix(config.accountId, Constants.KEY_FIRST_TS),
firstRequestTs
)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix inconsistent StorageHelper usage pattern.

The getter uses StorageHelper.getIntFromPrefs(context, config, key, default) while the setter uses StorageHelper.putInt(context, StorageHelper.storageKeyWithSuffix(config.accountId, key), value). This inconsistency could lead to reading/writing different preference keys.

Apply this fix to make the patterns consistent:

 fun getFirstRequestTs() : Int {
     return StorageHelper.getIntFromPrefs(
         context,
         config,
-        Constants.KEY_FIRST_TS,
+        StorageHelper.storageKeyWithSuffix(config.accountId, Constants.KEY_FIRST_TS),
         0
     )
 }

Or alternatively, modify the setter to use the same pattern as the getter.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fun getFirstRequestTs() : Int {
return StorageHelper.getIntFromPrefs(
context,
config,
Constants.KEY_FIRST_TS,
0
)
}
fun setFirstRequestTs(firstRequestTs: Int) {
StorageHelper.putInt(
context,
StorageHelper.storageKeyWithSuffix(config.accountId, Constants.KEY_FIRST_TS),
firstRequestTs
)
}
fun getFirstRequestTs() : Int {
return StorageHelper.getIntFromPrefs(
context,
config,
StorageHelper.storageKeyWithSuffix(config.accountId, Constants.KEY_FIRST_TS),
0
)
}
🤖 Prompt for AI Agents
In
clevertap-core/src/main/java/com/clevertap/android/sdk/network/http/NetworkRepo.kt
between lines 14 and 28, the getter and setter for firstRequestTs use
inconsistent StorageHelper methods that may access different preference keys. To
fix this, modify the setter to use StorageHelper.putInt with the same key
parameter as the getter, avoiding the use of storageKeyWithSuffix, so both
methods read and write to the same preference key consistently.

Comment on lines 110 to 122
val randomGen = SecureRandom()
val randomDelay = (randomGen.nextInt(10) + 1) * 1000
val delayBy = currentDelay + randomDelay
if (delayBy < Constants.MAX_DELAY_FREQUENCY) {
config.logger.debug(
config.accountId,
"Setting delay frequency to $currentDelay"
)
return delayBy
} else {
return Constants.PUSH_DELAY_MS
}
}
Copy link
Contributor

@coderabbitai coderabbitai bot Jun 2, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix incorrect fallback delay value.

The logic incorrectly returns Constants.PUSH_DELAY_MS instead of the calculated delayBy when the delay exceeds the maximum threshold. This negates the backoff calculation.

Apply this fix:

             if (delayBy < Constants.MAX_DELAY_FREQUENCY) {
                 config.logger.debug(
                     config.accountId,
-                    "Setting delay frequency to $currentDelay"
+                    "Setting delay frequency to $delayBy"
                 )
                 return delayBy
             } else {
-                return Constants.PUSH_DELAY_MS
+                return Constants.MAX_DELAY_FREQUENCY
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
val randomGen = SecureRandom()
val randomDelay = (randomGen.nextInt(10) + 1) * 1000
val delayBy = currentDelay + randomDelay
if (delayBy < Constants.MAX_DELAY_FREQUENCY) {
config.logger.debug(
config.accountId,
"Setting delay frequency to $currentDelay"
)
return delayBy
} else {
return Constants.PUSH_DELAY_MS
}
}
val randomGen = SecureRandom()
val randomDelay = (randomGen.nextInt(10) + 1) * 1000
val delayBy = currentDelay + randomDelay
if (delayBy < Constants.MAX_DELAY_FREQUENCY) {
config.logger.debug(
config.accountId,
"Setting delay frequency to $delayBy"
)
return delayBy
} else {
return Constants.MAX_DELAY_FREQUENCY
}
}
🤖 Prompt for AI Agents
In
clevertap-core/src/main/java/com/clevertap/android/sdk/network/http/NetworkRepo.kt
between lines 110 and 122, the fallback logic incorrectly returns
Constants.PUSH_DELAY_MS when the calculated delay exceeds the maximum allowed
delay, which bypasses the intended backoff calculation. To fix this, modify the
else branch to return Constants.MAX_DELAY_FREQUENCY instead of
Constants.PUSH_DELAY_MS, ensuring the delay does not exceed the maximum
threshold but still respects the backoff logic.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

fixed this in new commit.

Copy link
Contributor

Choose a reason for hiding this comment

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

@CTLalit Thank you for fixing the delay frequency logic! Great to see the fallback now properly returns MAX_DELAY_FREQUENCY and the log message shows the correct delayBy value. This ensures the backoff calculation works as intended.

🐰✨
╭─────╮
│ 🎉 │
╰─────╯
\(^o^)/


🧠 Learnings used
Learnt from: CTLalit
PR: CleverTap/clevertap-android-sdk#798
File: clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt:152-152
Timestamp: 2025-05-30T08:21:16.962Z
Learning: CTLalit prefers to avoid modifying legacy code when working on specific features, keeping the scope focused to reduce risk and maintain clear boundaries for changes.

If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

CTLalit added 2 commits June 2, 2025 13:55
- adds test cases to network repo class
- covers domains, mute, delay frequency
- uses repo instead of direct access to sp
- adds missing spiky domain tests
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (14)
clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt (10)

110-115: Poor exception handling in legacy code section.

The exception is caught but not properly logged with exception details, making debugging difficult. However, based on previous feedback, this appears to be legacy code that should remain unchanged to maintain focused scope.


644-651: Inconsistent exception handling in push notification processing.

The exception handling here uses different patterns - JSONException gets a generic verbose log while the generic Exception uses printStackTrace(). This inconsistency makes debugging difficult.


558-561: Missing logging for decryption failures.

When decryption fails, the method returns false without logging the failure reason, making troubleshooting difficult.


616-618: JSONException silently swallowed.

The JSONException is caught but only has a //skip comment, which hides important parsing errors.


718-730: Deprecated network connectivity API usage.

The method uses the deprecated activeNetworkInfo API and optimistically returns true on exceptions, which can mask real connectivity issues.


69-77: ⚠️ Potential issue

Thread-safety issue with mNetworkHeadersListeners collection.

The ArrayList used for mNetworkHeadersListeners is not thread-safe. This can cause ConcurrentModificationException when listeners are added/removed while being iterated in notifyNetworkHeaderListeners.

Replace with thread-safe collection:

-    private val mNetworkHeadersListeners: MutableList<NetworkHeadersListener> = ArrayList()
+    private val mNetworkHeadersListeners = CopyOnWriteArrayList<NetworkHeadersListener>()

416-442: 🛠️ Refactor suggestion

Silent fallback to unencrypted requests on encryption failure.

When encryption fails, the method silently falls back to sending unencrypted requests without documenting this behavior or making it configurable. This could be a security concern for applications requiring strict encryption.

Consider adding configuration to control this fallback behavior and document it clearly in the SDK documentation.


69-77: Thread safety issue with NetworkHeadersListeners

The mNetworkHeadersListeners uses ArrayList which is not thread-safe. This can cause ConcurrentModificationException when addNetworkHeadersListener()/removeNetworkHeadersListener() are called from the main thread while notifyNetworkHeaderListeners iterates on a worker thread.


110-115: 🛠️ Refactor suggestion

Improve exception handling to preserve stack trace

The exception is caught but only a generic message is logged without the actual exception details, making debugging difficult.

Consider logging the exception details:

} catch (e: Exception) {
    config.logger.verbose(
        config.accountId,
-       "met with exception while notifying listeners for PushImpressionSentToServer event"
+       "Exception while notifying listeners for PushImpressionSentToServer event",
+       e
    )
}

649-651: 🛠️ Refactor suggestion

Replace printStackTrace with proper logging

The generic exception handling uses printStackTrace() which doesn't integrate with the logging infrastructure and may not be captured in production environments.

Replace with consistent logging:

} catch (e: Exception) {
-   e.printStackTrace()
+   logger.verbose(
+       config.accountId,
+       "Unexpected exception while processing push notification queue",
+       e
+   )
clevertap-core/src/test/java/com/clevertap/android/sdk/network/NetworkManagerTest.kt (4)

514-518: Consider using relaxed mockk instead of spyk for CleverTapResponse.

Using spyk<CleverTapResponse>() may cause runtime issues if CleverTapResponse is an interface or abstract class. A relaxed mockk would be safer while still allowing verification.


68-71: Missing MockK interaction clearing in tearDown.

The tearDown method should clear MockK interactions to prevent cross-test contamination, especially when using verify(exactly = 0) assertions.

Add clearAllMocks() to prevent interaction history from affecting subsequent tests.


68-71: Add MockK cleanup to prevent cross-test contamination

The tearDown method handles Mockito cleanup but doesn't clear MockK interactions, which can cause test failures due to interaction history from previous tests.


514-518: Replace spy usage with relaxed mock for CleverTapResponse

Using spyk<CleverTapResponse>() can cause runtime exceptions if CleverTapResponse is an interface or abstract class with unimplemented members.

🧹 Nitpick comments (5)
clevertap-core/src/main/java/com/clevertap/android/sdk/network/api/CtApiWrapper.kt (1)

18-18: Fix KDoc parameter description.

The KDoc parameter description still refers to "The application context" but the parameter is now networkRepo of type NetworkRepo.

- * @param networkRepo The application context.
+ * @param networkRepo The network repository for managing network-related persistent state.
clevertap-core/src/test/java/com/clevertap/android/sdk/network/http/NetworkRepoTest.kt (1)

116-148: Good coverage for mute state management.

The mute state tests verify both true and false scenarios. However, the tests could be more robust by directly verifying the stored values rather than just checking for successful completion.

Consider adding helper methods or additional assertions to verify the actual stored mute state values:

// Example of more robust verification
@Test
fun `setMuted with true should store non-zero timestamp`() {
    // Given
    networkRepo = NetworkRepo(appCtx, config)
    val beforeTime = (System.currentTimeMillis() / 1000).toInt()
    
    // When
    networkRepo.setMuted(true)
    
    // Then - Verify a non-zero value was stored by checking behavior
    // Could add a getMutedTimestamp method for direct verification
    val afterTime = (System.currentTimeMillis() / 1000).toInt()
    assertTrue("Mute operation should complete successfully", true)
}
clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkRepo.kt (1)

102-134: Review delay calculation logic and logging accuracy.

The delay calculation logic is sound, but there's a logging inconsistency on line 127 where it logs currentDelay but should log delayBy (the calculated delay that's actually returned).

                 config.logger.debug(
                     config.accountId,
-                    "Setting delay frequency to $currentDelay"
+                    "Setting delay frequency to $delayBy"
                 )

Also, consider adding a log statement when falling back to PUSH_DELAY_MS due to exceeding MAX_DELAY_FREQUENCY:

             } else {
+                config.logger.debug(
+                    config.accountId,
+                    "Calculated delay $delayBy exceeds max delay, falling back to ${Constants.PUSH_DELAY_MS}"
+                )
                 return Constants.PUSH_DELAY_MS
             }
clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt (2)

557-561: Add logging for decryption failures

When decryption fails, the method returns false without logging the specific failure reason, which could make troubleshooting difficult.

Consider adding debug logging:

is EncryptionFailure -> {
+   logger.verbose(config.accountId, "Failed to decrypt response: ${decryptResponse.error}")
    return false
}

616-617: Improve exception handling for JSON parsing

The JSONException is silently ignored with only a comment, which could hide important parsing errors and make debugging difficult.

Consider adding proper logging:

} catch (jsonException: JSONException) {
-   //skip
+   logger.verbose(config.accountId, "Failed to parse event in queue", jsonException)
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bcb55a4 and 3b57eb8.

📒 Files selected for processing (7)
  • clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapFactory.kt (6 hunks)
  • clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt (1 hunks)
  • clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkRepo.kt (1 hunks)
  • clevertap-core/src/main/java/com/clevertap/android/sdk/network/api/CtApiWrapper.kt (4 hunks)
  • clevertap-core/src/test/java/com/clevertap/android/sdk/network/NetworkManagerTest.kt (6 hunks)
  • clevertap-core/src/test/java/com/clevertap/android/sdk/network/api/CtApiWrapperTest.kt (3 hunks)
  • clevertap-core/src/test/java/com/clevertap/android/sdk/network/http/NetworkRepoTest.kt (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapFactory.kt
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: CTLalit
PR: CleverTap/clevertap-android-sdk#798
File: clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt:152-152
Timestamp: 2025-05-30T08:21:16.962Z
Learning: CTLalit prefers to avoid modifying legacy code when working on specific features, keeping the scope focused to reduce risk and maintain clear boundaries for changes.
clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt (1)
Learnt from: CTLalit
PR: CleverTap/clevertap-android-sdk#798
File: clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt:558-558
Timestamp: 2025-05-30T08:41:45.328Z
Learning: In the CleverTap Android SDK, debug logging that includes request bodies is controlled by build configuration and only enabled in development builds, not in production. This build-time control already addresses potential sensitive data exposure concerns.
🔇 Additional comments (30)
clevertap-core/src/test/java/com/clevertap/android/sdk/network/api/CtApiWrapperTest.kt (2)

6-6: LGTM! Clean refactoring to support NetworkRepo abstraction.

The test class has been properly updated to use the new NetworkRepo abstraction instead of direct Context usage. The import, field declaration, and initialization are all correctly implemented.

Also applies to: 21-21, 29-30


52-54: Consistent constructor updates across all test methods.

All test methods that create CtApiWrapper instances have been consistently updated to use networkRepo instead of application. This maintains test consistency across the entire class.

clevertap-core/src/main/java/com/clevertap/android/sdk/network/api/CtApiWrapper.kt (2)

23-23: Excellent refactoring to use NetworkRepo abstraction.

The constructor and method signatures have been properly updated to use NetworkRepo instead of Context, which provides better separation of concerns and encapsulation of network-related state management.

Also applies to: 31-31, 49-49


62-63: Clean abstraction for domain retrieval.

The refactoring replaces direct SharedPreferences access with clean NetworkRepo method calls for retrieving cached domains. This improves maintainability and testability.

clevertap-core/src/test/java/com/clevertap/android/sdk/network/http/NetworkRepoTest.kt (4)

1-27: Excellent test class setup with comprehensive coverage.

The test class is well-structured with proper Robolectric setup and realistic CleverTap configuration instantiation. The setup method correctly creates real config instances that will use actual SharedPreferences through Robolectric.


28-115: Thorough testing of timestamp storage and retrieval.

The test methods for first and last request timestamps cover both positive cases (storing and retrieving values) and edge cases (default values when no data is stored). The test structure with clear given-when-then sections enhances readability.


266-404: Comprehensive delay frequency calculation testing.

The test coverage for getMinDelayFrequency is excellent, covering:

  • Retry counts below 10
  • Null account regions
  • Calculated delays with mocked random generation
  • Boundary conditions and edge cases
  • Default random delay validation

The use of mocked random delay generation allows for deterministic testing of the delay logic.


406-446: Valuable integration and isolation tests.

The integration test verifies that different storage operations work independently, and the test with different config instances ensures proper data isolation between different CleverTap accounts.

clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkRepo.kt (4)

9-16: Well-designed constructor with dependency injection.

The constructor design is excellent, accepting all necessary dependencies and providing a sensible default for the random delay generator using SecureRandom. The default random delay range of 1-10 seconds is appropriate for network retry scenarios.


51-66: Consistent storage approach for mute state.

The mute state methods correctly use the manual storageKeyWithSuffix approach consistently in both the true and false branches. This is implemented correctly.


18-32: ⚠️ Potential issue

Inconsistent storage key usage between get and set methods.

The getFirstRequestTs() method uses StorageHelper.getIntFromPrefs() which internally calls storageKeyWithSuffix, but setFirstRequestTs() manually calls StorageHelper.storageKeyWithSuffix(). This inconsistency could lead to different storage keys being used.

 fun setFirstRequestTs(firstRequestTs: Int) {
     StorageHelper.putInt(
         context,
-        StorageHelper.storageKeyWithSuffix(config.accountId, Constants.KEY_FIRST_TS),
+        Constants.KEY_FIRST_TS,
         firstRequestTs
     )
 }

Or alternatively, ensure both methods use the same approach consistently.

Likely an incorrect or invalid review comment.


68-83:

✅ Verification successful

Domain storage methods need consistency check.

The domain methods may have the same storage key inconsistency issue. Verify that getDomain() and setDomain() use compatible storage key generation.


🏁 Script executed:

#!/bin/bash
# Description: Check StorageHelper implementation to verify if getIntFromPrefs and getStringFromPrefs internally use storageKeyWithSuffix
echo "Checking StorageHelper implementation for key generation consistency..."

# Search for StorageHelper methods to understand key handling
ast-grep --pattern $'class StorageHelper {
  $$$
  getIntFromPrefs($$$) {
    $$$
  }
  $$$
}'

ast-grep --pattern $'class StorageHelper {
  $$$
  getStringFromPrefs($$$) {
    $$$
  }
  $$$
}'

# Also check for storageKeyWithSuffix usage
rg -A 5 -B 2 "storageKeyWithSuffix"

Length of output: 67234


No storage key mismatch for domain methods
Both setDomain() and getDomain() use the same key format (KEY_DOMAIN_NAME:<accountId>). setDomain explicitly calls storageKeyWithSuffix(accountId, KEY_DOMAIN_NAME), and getDomain’s getStringFromPrefs internally applies storageKeyWithSuffix(config, rawKey). No changes needed.

clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt (11)

46-61: Constructor complexity is high but may be justified for this central component.

The constructor has 12 dependencies which indicates this class has significant responsibilities. While this violates typical dependency injection guidelines, it may be acceptable for a central network manager that coordinates multiple subsystems.

Consider documenting the rationale for this design choice and ensure each dependency serves a distinct purpose.


295-295: Request body logging is appropriately controlled.

The debug logging includes the entire request body, but based on the build configuration, this is only enabled in development builds, not production. This appropriately addresses potential sensitive data exposure concerns.


189-191: Excellent refactoring of header construction.

The previous large getQueueHeader method has been properly refactored to use the QueueHeaderBuilder pattern, significantly improving maintainability and single responsibility adherence.


274-341: Complex but well-structured sendQueue method.

The method is quite long but handles the core network sending logic with proper error handling, response processing, and cleanup. The structure is clear with distinct sections for validation, API calls, and response handling.


592-602: Appropriate error response handling.

The method properly handles different HTTP error codes with specific logic for code 419 (relaxing network constraints) and includes proper logging.


555-568: Well-implemented encrypted response handling.

The encryption/decryption integration properly checks for the encryption header, handles both success and failure cases, and maintains the response processing flow.


673-699: Proper domain management implementation.

The domain setting methods properly update both the repository and API wrapper caches, and include appropriate callback notifications for domain changes.


701-715: Appropriate SDK muting implementation.

The muting logic properly clears queues when muted and uses async execution for database operations to avoid blocking the caller thread.


194-222: Handshake implementation looks solid

The handshake logic properly handles success/failure scenarios, domain changes, and mute commands with appropriate error handling and logging.


718-730: Network connectivity check uses deprecated API

The method uses the deprecated activeNetworkInfo API and returns optimistically true on exceptions, which could mask real connectivity issues.

Note: Based on previous feedback preferences to avoid modifying legacy-style code in feature-focused PRs, this may be intentionally left unchanged to maintain scope boundaries.


46-732: Well-architected network layer implementation

The new Kotlin NetworkManager successfully implements the encryption-in-transit feature with a clean dependency injection pattern. The integration with NetworkEncryptionManager, NetworkRepo, and other new components follows solid architectural principles.

Key strengths:

  • Clear separation of concerns between encryption, storage, and network operations
  • Comprehensive error handling and logging throughout
  • Proper integration with the new repository pattern
  • Support for different event types and encryption modes

Areas noted for future enhancement:

  • Thread safety for listener collections
  • Exception logging improvements
  • Encryption fallback configurability
clevertap-core/src/test/java/com/clevertap/android/sdk/network/NetworkManagerTest.kt (7)

52-71: Improved test setup with proper mock lifecycle management.

The test setup properly manages MockitoAnnotations lifecycle and integrates MockK for the new encryption manager dependency.


16-19: Excellent use of defined constants over hardcoded strings.

The tests now use properly defined header constants from CtApi.Companion instead of hardcoded strings, improving maintainability and reducing magic strings.

Also applies to: 83-84, 91-92


152-475: Comprehensive test coverage for new NetworkManager functionality.

The new test cases thoroughly cover:

  • Domain change handling
  • Mute header processing
  • Encryption/decryption scenarios
  • Special event detection (App Launched, wzrk_fetch)
  • Success and failure paths

The tests are well-structured with clear arrange-act-assert patterns and descriptive names.


368-449: Excellent encryption functionality testing.

The encryption test cases thoroughly cover:

  • Successful response decryption
  • Decryption failure handling
  • Missing encryption headers
  • False encryption headers

The MockK usage is correct with proper every/verify patterns.


521-537: Proper NetworkManager constructor usage with all dependencies.

The constructor call correctly provides all required dependencies including the new ones (encryption manager, network repo, queue header builder) with clear named parameters for readability.


368-449: Excellent encryption test coverage

The encryption-related test cases comprehensively cover the new encryption functionality including:

  • Successful decryption with proper processor interaction
  • Decryption failure scenarios
  • Header-based encryption detection logic
  • Both enabled and disabled encryption states

The MockK usage is proper and the test assertions verify the correct behavior flows.


152-223: Comprehensive domain and mute handling tests

The test cases thoroughly verify domain change detection and mute header processing functionality with proper setup/teardown of SharedPreferences state. Good coverage of both return values and side effects.

Comment on lines 416 to 442
private fun sendQueueApi(body: SendQueueRequestBody): Response {
if (config.isEncryptionInTransitEnabled && coreMetaData.isRelaxNetwork.not()) {
val encryptionResult = encryptionManager.encryptResponse(body.toString())
val sessionEncryptionKey = encryptionManager.sessionEncryptionKey()

if (encryptionResult is EncryptionSuccess) {
val bodyEnc = EncryptedSendQueueRequestBody(
encryptedPayload = encryptionResult.data,
key = sessionEncryptionKey,
iv = encryptionResult.iv
).toJsonString()
logger.verbose("Encrypted Request = $bodyEnc")
return ctApiWrapper.ctApi.sendQueue(
isViewedEvent = false,
body = bodyEnc,
isEncrypted = true
)
} else {
logger.verbose("Normal Request cause encryption failed = $body")
}
}
return ctApiWrapper.ctApi.sendQueue(
isViewedEvent = false,
body = body.toString(),
isEncrypted = false
)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Document encryption fallback behavior and consider configurability

The method silently falls back to unencrypted requests when encryption fails (lines 434-435). This behavior should be documented and potentially made configurable for security-sensitive applications.

Consider adding configuration to control fallback behavior and ensure it's documented:

private fun sendQueueApi(body: SendQueueRequestBody): Response {
    if (config.isEncryptionInTransitEnabled && coreMetaData.isRelaxNetwork.not()) {
        val encryptionResult = encryptionManager.encryptResponse(body.toString())
        
        if (encryptionResult is EncryptionSuccess) {
            // ... encrypted send logic
        } else {
            logger.verbose(config.accountId, "Encryption failed, falling back to unencrypted request")
            // Consider: if (config.strictEncryptionMode) throw SecurityException("Encryption required but failed")
        }
    }
    return ctApiWrapper.ctApi.sendQueue(/* ... */)
}
🤖 Prompt for AI Agents
In
clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt
between lines 416 and 442, the sendQueueApi method silently falls back to
sending unencrypted requests if encryption fails. To fix this, add documentation
explaining this fallback behavior and introduce a configuration option (e.g.,
strictEncryptionMode) to control whether to allow fallback or throw an exception
on encryption failure. Update the code to check this config and either proceed
with unencrypted requests or raise a SecurityException accordingly, and log the
fallback event with relevant details.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
clevertap-core/src/test/java/com/clevertap/android/sdk/network/NetworkManagerTest.kt (2)

67-71: Reset MockK interactions in tearDown() to avoid cross-test leakage

The same networkEncryptionManager mock instance is shared across all tests, yet its recorded interactions are never cleared.
Tests that verify call counts (verify(exactly = 0) { … }) can therefore fail or give false-positives depending on previous test execution order.


541-545: Replace spyk with a relaxed mockk for CleverTapResponse

spyk<CleverTapResponse>() tries to create a spy of the real class, calling the original (often abstract / open) methods.
If CleverTapResponse is an interface or an abstract class with un-implemented members, the spy will throw at runtime when processResponse is invoked by sendQueue.

Switching to a relaxed mock avoids this risk while still letting you verify the interaction.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c411136 and bab9ea1.

📒 Files selected for processing (1)
  • clevertap-core/src/test/java/com/clevertap/android/sdk/network/NetworkManagerTest.kt (6 hunks)
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: CTLalit
PR: CleverTap/clevertap-android-sdk#798
File: clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt:152-152
Timestamp: 2025-05-30T08:21:16.962Z
Learning: CTLalit prefers to avoid modifying legacy code when working on specific features, keeping the scope focused to reduce risk and maintain clear boundaries for changes.
🧬 Code Graph Analysis (1)
clevertap-core/src/test/java/com/clevertap/android/sdk/network/NetworkManagerTest.kt (3)
test_shared/src/main/java/com/clevertap/android/shared/test/BaseTestCase.kt (1)
  • getSampleJsonArrayOfJsonObjects (58-67)
clevertap-core/src/test/java/com/clevertap/android/sdk/network/IJRepoTest.kt (1)
  • assertFalse (654-656)
clevertap-core/src/main/java/com/clevertap/android/sdk/ControllerManager.java (1)
  • ControllerManager (23-243)
🔇 Additional comments (2)
clevertap-core/src/test/java/com/clevertap/android/sdk/network/NetworkManagerTest.kt (2)

367-473: Excellent encryption test coverage!

The encryption-related tests comprehensively cover all scenarios:

  • Successful decryption
  • Failed decryption
  • Missing encryption header
  • False encryption header

The tests properly verify mock interactions and assert expected behavior.


475-501: Good coverage of specific HTTP error codes

The tests for 402 (Payment Required) and 419 (Authentication Timeout) properly verify that sendQueue returns false for these error conditions.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (3)
clevertap-core/src/test/java/com/clevertap/android/sdk/network/NetworkManagerTest.kt (3)

67-70: Reset MockK interactions in tearDown() to avoid cross-test leakage.

The same networkEncryptionManager mock instance is shared across all tests, yet its recorded interactions are never cleared. Tests that verify call counts (verify(exactly = 0) { … }) can therefore fail or give false-positives depending on previous test execution order.

 @After
 fun tearDown() {
     closeable?.close()
+    // Ensures mocks don't carry interaction history into the next test run
+    clearAllMocks()   // <-- import io.mockk.clearAllMocks
 }

184-188: Fix incorrect storage key construction.

The storage key construction is inconsistent with line 167. It should use the full config object, not just the accountId.

Apply this diff to fix the storage key:

-        StorageHelper.putString(
-            appCtx,
-            StorageHelper.storageKeyWithSuffix(cleverTapInstanceConfig.accountId, NetworkRepo.KEY_DOMAIN_NAME),
-            originalDomain
-        )
+        StorageHelper.putString(
+            appCtx,
+            StorageHelper.storageKeyWithSuffix(cleverTapInstanceConfig, NetworkRepo.KEY_DOMAIN_NAME),
+            originalDomain
+        )

541-545: Replace spyk with a relaxed mockk for CleverTapResponse.

spyk<CleverTapResponse>() tries to create a spy of the real class, calling the original (often abstract / open) methods. If CleverTapResponse is an interface or an abstract class with un-implemented members, the spy will throw at runtime when processResponse is invoked by sendQueue.

Switching to a relaxed mock avoids this risk while still letting you verify the interaction.

-for (i in 1..10) {
-    val spyProcessor = spyk<CleverTapResponse>()
-    responses.add(spyProcessor)
-}
+for (i in 1..10) {
+    val processor = mockk<CleverTapResponse>(relaxed = true)
+    responses.add(processor)
+}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bcca57c and f74f8e0.

📒 Files selected for processing (4)
  • clevertap-core/src/main/java/com/clevertap/android/sdk/Constants.java (0 hunks)
  • clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkRepo.kt (1 hunks)
  • clevertap-core/src/test/java/com/clevertap/android/sdk/network/NetworkManagerTest.kt (6 hunks)
  • clevertap-core/src/test/java/com/clevertap/android/sdk/network/http/NetworkRepoTest.kt (1 hunks)
💤 Files with no reviewable changes (1)
  • clevertap-core/src/main/java/com/clevertap/android/sdk/Constants.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkRepo.kt
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: CTLalit
PR: CleverTap/clevertap-android-sdk#798
File: clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt:152-152
Timestamp: 2025-05-30T08:21:16.962Z
Learning: CTLalit prefers to avoid modifying legacy code when working on specific features, keeping the scope focused to reduce risk and maintain clear boundaries for changes.
🔇 Additional comments (6)
clevertap-core/src/test/java/com/clevertap/android/sdk/network/http/NetworkRepoTest.kt (2)

413-433: Good coverage of randomness testing.

The test effectively verifies that the default random delay generator produces varied results within the expected range. The approach of testing multiple calls and checking for uniqueness is a solid way to validate randomness behavior.


461-475: Excellent test for config isolation.

This test correctly verifies that different CleverTap instance configurations maintain separate storage, which is crucial for multi-account scenarios. The test design and assertions are appropriate.

clevertap-core/src/test/java/com/clevertap/android/sdk/network/NetworkManagerTest.kt (4)

264-292: Good test coverage for App Launched event handling.

The test correctly verifies that when an App Launched event is present in the queue, all response processors receive the isFullResponse=true flag. The test structure and assertions are appropriate for this scenario.


368-397: Comprehensive encryption testing with proper mocking.

This test effectively verifies the encryption flow by mocking the NetworkEncryptionManager to return successful decryption and then validating that:

  1. Decryption is called when the encryption header is present
  2. The decrypted response is passed to processors
  3. The method returns true for successful processing

403-425: Good negative test case for encryption failure.

This test correctly handles the scenario where decryption fails, ensuring that sendQueue returns false and that the decryption attempt was made. This is important for robust error handling.


476-501: Good coverage of specific HTTP error codes.

The tests for HTTP 402 and 419 error codes are valuable additions that ensure proper handling of these specific error scenarios. The test structure is consistent and the assertions are correct.

Comment on lines +146 to +176
@Test
fun `setMuted with true should store current timestamp`() {
// Given
networkRepo = NetworkRepo(appCtx, config)
val beforeTime = (System.currentTimeMillis() / 1000).toInt()

// When
networkRepo.setMuted(true)
val afterTime = (System.currentTimeMillis() / 1000).toInt()

// Then - Verify by checking that a non-zero timestamp was stored
// We can't directly access the muted value, but we can verify the behavior
// by calling setMuted(false) and then setMuted(true) again
networkRepo.setMuted(false)
networkRepo.setMuted(true)

// The test passes if no exception is thrown and the method completes successfully
assertTrue("setMuted(true) should complete without error", true)
}

@Test
fun `setMuted with false should store zero`() {
// Given
networkRepo = NetworkRepo(appCtx, config)

// When
networkRepo.setMuted(false)

// Then - The test passes if no exception is thrown
assertTrue("setMuted(false) should complete without error", true)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Improve mute state testing to verify actual behavior.

The current mute tests don't verify the actual muted state or timestamp values. They only check that the methods complete without exceptions, which provides limited value.

Consider enhancing the tests to verify the actual stored values:

 @Test
 fun `setMuted with true should store current timestamp`() {
     // Given
     networkRepo = NetworkRepo(appCtx, config)
     val beforeTime = (System.currentTimeMillis() / 1000).toInt()

     // When
     networkRepo.setMuted(true)
     val afterTime = (System.currentTimeMillis() / 1000).toInt()

     // Then - Verify by checking that a non-zero timestamp was stored
-    // We can't directly access the muted value, but we can verify the behavior
-    // by calling setMuted(false) and then setMuted(true) again
-    networkRepo.setMuted(false)
-    networkRepo.setMuted(true)
-
-    // The test passes if no exception is thrown and the method completes successfully
-    assertTrue("setMuted(true) should complete without error", true)
+    // Verify the muted timestamp is stored in the expected range
+    val storedMutedTime = StorageHelper.getIntFromPrefs(
+        appCtx,
+        config,
+        Constants.KEY_MUTED,
+        0
+    )
+    assertTrue("Muted timestamp should be between beforeTime and afterTime", 
+        storedMutedTime >= beforeTime && storedMutedTime <= afterTime)
 }

 @Test
 fun `setMuted with false should store zero`() {
     // Given
     networkRepo = NetworkRepo(appCtx, config)
+    // First set muted to true
+    networkRepo.setMuted(true)

     // When
     networkRepo.setMuted(false)

-    // Then - The test passes if no exception is thrown
-    assertTrue("setMuted(false) should complete without error", true)
+    // Then - Verify muted timestamp is cleared
+    val storedMutedTime = StorageHelper.getIntFromPrefs(
+        appCtx,
+        config,
+        Constants.KEY_MUTED,
+        0
+    )
+    assertEquals("setMuted(false) should store 0", 0, storedMutedTime)
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Test
fun `setMuted with true should store current timestamp`() {
// Given
networkRepo = NetworkRepo(appCtx, config)
val beforeTime = (System.currentTimeMillis() / 1000).toInt()
// When
networkRepo.setMuted(true)
val afterTime = (System.currentTimeMillis() / 1000).toInt()
// Then - Verify by checking that a non-zero timestamp was stored
// We can't directly access the muted value, but we can verify the behavior
// by calling setMuted(false) and then setMuted(true) again
networkRepo.setMuted(false)
networkRepo.setMuted(true)
// The test passes if no exception is thrown and the method completes successfully
assertTrue("setMuted(true) should complete without error", true)
}
@Test
fun `setMuted with false should store zero`() {
// Given
networkRepo = NetworkRepo(appCtx, config)
// When
networkRepo.setMuted(false)
// Then - The test passes if no exception is thrown
assertTrue("setMuted(false) should complete without error", true)
}
@Test
fun `setMuted with true should store current timestamp`() {
// Given
networkRepo = NetworkRepo(appCtx, config)
val beforeTime = (System.currentTimeMillis() / 1000).toInt()
// When
networkRepo.setMuted(true)
val afterTime = (System.currentTimeMillis() / 1000).toInt()
// Then - Verify by checking that a non-zero timestamp was stored
// Verify the muted timestamp is stored in the expected range
val storedMutedTime = StorageHelper.getIntFromPrefs(
appCtx,
config,
Constants.KEY_MUTED,
0
)
assertTrue(
"Muted timestamp should be between beforeTime and afterTime",
storedMutedTime >= beforeTime && storedMutedTime <= afterTime
)
}
@Test
fun `setMuted with false should store zero`() {
// Given
networkRepo = NetworkRepo(appCtx, config)
// First set muted to true
networkRepo.setMuted(true)
// When
networkRepo.setMuted(false)
// Then - Verify muted timestamp is cleared
val storedMutedTime = StorageHelper.getIntFromPrefs(
appCtx,
config,
Constants.KEY_MUTED,
0
)
assertEquals("setMuted(false) should store 0", 0, storedMutedTime)
}
🤖 Prompt for AI Agents
In
clevertap-core/src/test/java/com/clevertap/android/sdk/network/http/NetworkRepoTest.kt
between lines 146 and 176, the tests for setMuted(true) and setMuted(false) only
check for method completion without errors but do not verify the actual muted
state or timestamp values. To fix this, enhance the tests by accessing the
stored muted state or timestamp after calling setMuted and assert that the
timestamp is correctly set to a non-zero value when muted is true and zero when
muted is false. This will ensure the tests validate the actual behavior rather
than just method execution.

feat(SDK-4709): adds coverage for CleverTapInstanceConfig

- uses string numeric values similar to other flags in the manifest.
- adds unit test coverage for CleverTapInstanceConfig
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
clevertap-core/src/test/java/com/clevertap/android/sdk/CleverTapFixtures.kt (1)

9-35: Consider simplifying the documentation for better readability.

The documentation is comprehensive but quite verbose. Consider condensing it while retaining essential information.

 /**
  * Creates a ManifestInfo instance with default parameters for testing.
- * 
- * @param accountId Account ID (default: Constant.ACC_ID)
- * @param accountToken Account token (default: Constant.ACC_TOKEN)
- * @param accountRegion Account region (default: null)
+ *
+ * @param accountId Account ID (default: Constant.ACC_ID)
+ * @param accountToken Account token (default: Constant.ACC_TOKEN)  
+ * @param accountRegion Account region (default: null)
  * @param proxyDomain Proxy domain (default: "")
  * @param spikyProxyDomain Spiky proxy domain (default: "")
- * @param handshakeDomain Handshake domain (default: "")
- * @param useADID Use Google Ad ID (default: false)
- * @param appLaunchedDisabled App launched disabled (default: false)
- * @param notificationIcon Notification icon (default: "notification icon")
- * @param excludedActivitiesForInApps Excluded activities for in-apps (default: null)
- * @param sslPinning SSL pinning enabled (default: false)
- * @param backgroundSync Background sync enabled (default: false)
- * @param useCustomID Use custom ID (default: false)
- * @param fcmSenderId FCM sender ID (default: "fcm:sender:id")
- * @param packageName Package name (default: "some.app.package")
- * @param beta Beta mode enabled (default: false)
- * @param intentServiceName Intent service name (default: "serviceName")
- * @param devDefaultPushChannelId Default push channel ID (default: "push-channel-id")
- * @param profileKeys Profile keys array (default: emptyArray())
- * @param encryptionLevel Encryption level (default: 0)
- * @param provider1 Provider 1 configuration (default: HMS configuration)
- * @param provider2 Provider 2 configuration (default: null)
- * @param encryptionInTransit Encryption in transit (default: "0")
+ * @param encryptionInTransit Encryption in transit flag (default: "0")
  */
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f74f8e0 and e3c21e8.

📒 Files selected for processing (4)
  • clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapInstanceConfig.java (9 hunks)
  • clevertap-core/src/main/java/com/clevertap/android/sdk/ManifestInfo.java (6 hunks)
  • clevertap-core/src/test/java/com/clevertap/android/sdk/CleverTapFixtures.kt (1 hunks)
  • clevertap-core/src/test/java/com/clevertap/android/sdk/CleverTapInstanceConfigTest.kt (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • clevertap-core/src/main/java/com/clevertap/android/sdk/ManifestInfo.java
  • clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapInstanceConfig.java
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: CTLalit
PR: CleverTap/clevertap-android-sdk#798
File: clevertap-core/src/main/java/com/clevertap/android/sdk/network/NetworkManager.kt:152-152
Timestamp: 2025-05-30T08:21:16.962Z
Learning: CTLalit prefers to avoid modifying legacy code when working on specific features, keeping the scope focused to reduce risk and maintain clear boundaries for changes.
🔇 Additional comments (7)
clevertap-core/src/test/java/com/clevertap/android/sdk/CleverTapFixtures.kt (2)

36-86: Well-designed factory method for test fixtures.

The factory method provides excellent flexibility for testing by allowing selective parameter overrides while maintaining sensible defaults. The inclusion of the new encryptionInTransit parameter with a default value of "0" aligns well with the encryption-in-transit feature introduction.


88-88: Good backward compatibility maintenance.

The update preserves the existing API while leveraging the new factory method, ensuring that existing tests continue to work without modification.

clevertap-core/src/test/java/com/clevertap/android/sdk/CleverTapInstanceConfigTest.kt (5)

8-9: LGTM! Appropriate test imports added.

The addition of assertFalse and assertTrue imports is necessary for the new encryption-in-transit test methods.


73-84: Well-implemented test for encryption enabled scenario.

The test correctly verifies that when encryptionInTransit is set to "1", the isEncryptionInTransitEnabled property returns true. Good use of descriptive test naming and clear assertion.


86-96: Good test coverage for encryption disabled scenario.

The test properly validates that when encryptionInTransit is set to "0", the encryption feature is disabled. This ensures the boolean conversion logic works correctly for the "false" case.


98-108: Excellent defensive testing for invalid input.

This test ensures robust behavior by verifying that invalid string values for encryptionInTransit default to disabled state. This is important for preventing unexpected behavior with malformed configuration values.


110-155: Comprehensive serialization test with good property coverage.

This test thoroughly validates that all CleverTapInstanceConfig properties, including the new encryptionInTransit field, are preserved through JSON serialization/deserialization cycles. The test covers all boolean flags and key properties, which is essential for ensuring configuration persistence works correctly.

The TODO comment on lines 151-152 indicates a known issue with identity keys serialization that's outside the scope of this encryption feature.

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.

1 participant