Backend/feat: Add Kernel.External.Moengage for S2S event tracking#1217
Backend/feat: Add Kernel.External.Moengage for S2S event tracking#1217witcher-shailesh wants to merge 1 commit intomainfrom
Conversation
|
Warning Rate limit exceeded
Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 20 minutes and 13 seconds. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (9)
WalkthroughIntroduces a new MoEngage S2S API integration module ( Changes
Sequence DiagramsequenceDiagram
participant Caller
participant Interface as Interface Layer
participant Flow as Flow Layer
participant API as MoEngage API
Caller->>Interface: pushEvent(config, request)
Interface->>Flow: pushEvent(MoengageCfg, MoengageEventReq)
Flow->>Flow: Decrypt apiSecret
Flow->>Flow: Build Basic Auth Header
Flow->>API: POST /v1/event with auth
API-->>Flow: MoengageEventResp or error
alt Success
Flow-->>Interface: MoengageEventResp
else Failure
Flow-->>Interface: InternalError wrapped in monad
end
Interface-->>Caller: m MoengageEventResp
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Move ride event tracking from mobile app to backend for reliability. Integrates with Moengage S2S API via MerchantServiceConfig pattern. Events tracked: - ny_user_first_ride_completed (first ride detection) - ny_rider_ride_completed (with ride count) - driver_assigned (driver assignment to booking) - ny_user_source_and_destination (user search with lat/lon) - ny_user_request_quotes (estimate count) Depends on: nammayatri/shared-kernel#1217 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@lib/mobility-core/src/Kernel/External/Moengage.hs`:
- Around line 21-22: This module is missing the required custom Prelude import
under NoImplicitPrelude; add an explicit import of Kernel.Prelude (or
EulerHS.Prelude per project convention) to the top of Kernel.External.Moengage
(the module that imports Kernel.External.Moengage.Interface and
Kernel.External.Moengage.Types) so the file uses the repo's Prelude replacement
instead of the standard Prelude and complies with the coding guideline.
In `@lib/mobility-core/src/Kernel/External/Moengage/Moengage/API.hs`:
- Around line 25-31: The MoengageEventAPI type incorrectly includes a
MandatoryQueryParam "app_id" — remove the MandatoryQueryParam "app_id" Text from
the type so the API only uses the Capture "app_id" Text path parameter; update
the MoengageEventAPI declaration to omit the query param reference and ensure
any related imports/usages referencing MandatoryQueryParam for this endpoint are
adjusted accordingly.
In `@lib/mobility-core/src/Kernel/External/Moengage/Moengage/Config.hs`:
- Around line 21-27: The derived Show on MoengageCfg can leak the apiSecret;
remove Show from the deriving list for MoengageCfg and provide a safe Show
instance that redacts or omits apiSecret (e.g., show other fields and display
"<redacted>" or omit the apiSecret field) so that calling show on MoengageCfg
cannot expose secrets; reference the MoengageCfg type and its apiSecret field
and keep other derived instances (Eq, Generic, ToJSON, FromJSON) unchanged.
In `@lib/mobility-core/src/Kernel/External/Moengage/Moengage/Flow.hs`:
- Around line 44-49: The pushEvent function currently always calls the MoEngage
API; update pushEvent to respect cfg.enabled by checking it at the top and
short-circuiting when disabled (i.e., do not decrypt, build auth, or call
callAPI). If cfg.enabled is False, return the appropriate successful/no-op
result for pushEvent (or log and return success) instead of performing the
external call; otherwise proceed as now (keep existing logic using
moengageClient, buildBasicAuth, callAPI and fromEitherM). Ensure you reference
cfg.enabled and preserve existing error handling paths
(fromEitherM/InternalError) when enabled.
- Line 49: The current error mapping in Flow.hs uses fromEitherM (\err ->
InternalError $ "Failed to call Moengage Event API: " <> show err), exposing raw
ClientError details; change it to return a generic, non-sensitive InternalError
message (e.g. "Failed to call Moengage Event API") without using show on the
error, and if needed capture/redact specific safe metadata in a separate
audit/log path (never inline the full show err). Update the mapping that
produces InternalError (the lambda passed to fromEitherM) to omit sensitive
details and apply the same replacement pattern to other external integrations
(Wallet, Ticket, Tokenize, Insurance, Payment, Payout, Maps) where fromEitherM
or similar error-to-InternalError conversions use show on low-level errors.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 0ad6f640-81e2-408a-a357-3a6a69bc5763
📒 Files selected for processing (8)
lib/mobility-core/src/Kernel/External/Moengage.hslib/mobility-core/src/Kernel/External/Moengage/Interface.hslib/mobility-core/src/Kernel/External/Moengage/Interface/Types.hslib/mobility-core/src/Kernel/External/Moengage/Moengage/API.hslib/mobility-core/src/Kernel/External/Moengage/Moengage/Config.hslib/mobility-core/src/Kernel/External/Moengage/Moengage/Flow.hslib/mobility-core/src/Kernel/External/Moengage/Moengage/Types.hslib/mobility-core/src/Kernel/External/Moengage/Types.hs
| import Kernel.External.Moengage.Interface | ||
| import Kernel.External.Moengage.Types |
There was a problem hiding this comment.
Add the required Prelude import for this module.
This module currently skips Kernel.Prelude/EulerHS.Prelude, which violates the repo rule for Haskell modules.
✅ Minimal fix
import Kernel.External.Moengage.Interface
import Kernel.External.Moengage.Types
+import Kernel.Prelude ()As per coding guidelines, "All modules must import Kernel.Prelude or EulerHS.Prelude instead of the standard Prelude due to NoImplicitPrelude language pragma".
📝 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.
| import Kernel.External.Moengage.Interface | |
| import Kernel.External.Moengage.Types | |
| import Kernel.External.Moengage.Interface | |
| import Kernel.External.Moengage.Types | |
| import Kernel.Prelude () |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/mobility-core/src/Kernel/External/Moengage.hs` around lines 21 - 22, This
module is missing the required custom Prelude import under NoImplicitPrelude;
add an explicit import of Kernel.Prelude (or EulerHS.Prelude per project
convention) to the top of Kernel.External.Moengage (the module that imports
Kernel.External.Moengage.Interface and Kernel.External.Moengage.Types) so the
file uses the repo's Prelude replacement instead of the standard Prelude and
complies with the coding guideline.
| data MoengageCfg = MoengageCfg | ||
| { baseUrl :: BaseUrl, | ||
| appId :: Text, | ||
| apiSecret :: EncryptedField 'AsEncrypted Text, | ||
| enabled :: Bool | ||
| } | ||
| deriving (Show, Eq, Generic, ToJSON, FromJSON) |
There was a problem hiding this comment.
Avoid deriving Show for secret-bearing config types.
Line 24 contains apiSecret; derived Show can leak credential material into logs (even encrypted form should be treated as sensitive).
🔐 Safer change
data MoengageCfg = MoengageCfg
{ baseUrl :: BaseUrl,
appId :: Text,
apiSecret :: EncryptedField 'AsEncrypted Text,
enabled :: Bool
}
- deriving (Show, Eq, Generic, ToJSON, FromJSON)
+ deriving (Eq, Generic, ToJSON, FromJSON)📝 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.
| data MoengageCfg = MoengageCfg | |
| { baseUrl :: BaseUrl, | |
| appId :: Text, | |
| apiSecret :: EncryptedField 'AsEncrypted Text, | |
| enabled :: Bool | |
| } | |
| deriving (Show, Eq, Generic, ToJSON, FromJSON) | |
| data MoengageCfg = MoengageCfg | |
| { baseUrl :: BaseUrl, | |
| appId :: Text, | |
| apiSecret :: EncryptedField 'AsEncrypted Text, | |
| enabled :: Bool | |
| } | |
| deriving (Eq, Generic, ToJSON, FromJSON) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/mobility-core/src/Kernel/External/Moengage/Moengage/Config.hs` around
lines 21 - 27, The derived Show on MoengageCfg can leak the apiSecret; remove
Show from the deriving list for MoengageCfg and provide a safe Show instance
that redacts or omits apiSecret (e.g., show other fields and display
"<redacted>" or omit the apiSecret field) so that calling show on MoengageCfg
cannot expose secrets; reference the MoengageCfg type and its apiSecret field
and keep other derived instances (Eq, Generic, ToJSON, FromJSON) unchanged.
| pushEvent cfg req = do | ||
| apiSecret <- decrypt cfg.apiSecret | ||
| let authToken = buildBasicAuth cfg.appId apiSecret | ||
| moengageClient = client (Proxy :: Proxy MoengageEventAPI) | ||
| callAPI cfg.baseUrl (moengageClient cfg.appId cfg.appId (Just authToken) req) "moengageEvent" moengageEventAPI | ||
| >>= fromEitherM (\err -> InternalError $ "Failed to call Moengage Event API: " <> show err) |
There was a problem hiding this comment.
Respect cfg.enabled before sending events.
Line 48 executes the external call unconditionally; disabled integrations can still emit traffic.
🧩 Suggested guard
pushEvent cfg req = do
+ when (not cfg.enabled) $
+ throwError $ InternalError "Moengage integration is disabled"
apiSecret <- decrypt cfg.apiSecret
let authToken = buildBasicAuth cfg.appId apiSecret
moengageClient = client (Proxy :: Proxy MoengageEventAPI)
callAPI cfg.baseUrl (moengageClient cfg.appId cfg.appId (Just authToken) req) "moengageEvent" moengageEventAPI
>>= fromEitherM (\err -> InternalError $ "Failed to call Moengage Event API: " <> show err)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/mobility-core/src/Kernel/External/Moengage/Moengage/Flow.hs` around lines
44 - 49, The pushEvent function currently always calls the MoEngage API; update
pushEvent to respect cfg.enabled by checking it at the top and short-circuiting
when disabled (i.e., do not decrypt, build auth, or call callAPI). If
cfg.enabled is False, return the appropriate successful/no-op result for
pushEvent (or log and return success) instead of performing the external call;
otherwise proceed as now (keep existing logic using moengageClient,
buildBasicAuth, callAPI and fromEitherM). Ensure you reference cfg.enabled and
preserve existing error handling paths (fromEitherM/InternalError) when enabled.
| let authToken = buildBasicAuth cfg.appId apiSecret | ||
| moengageClient = client (Proxy :: Proxy MoengageEventAPI) | ||
| callAPI cfg.baseUrl (moengageClient cfg.appId cfg.appId (Just authToken) req) "moengageEvent" moengageEventAPI | ||
| >>= fromEitherM (\err -> InternalError $ "Failed to call Moengage Event API: " <> show err) |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In servant-client, does showonClientErrorinclude request headers (for exampleAuthorization)? Please share source/docs for the exact rendered fields.
💡 Result:
No, the show instance for ClientError does not include sensitive request headers like Authorization. Since servant-client-core 0.16 (servant 0.16), the FailureResponse constructor includes the failing RequestF (BaseUrl, ByteString), which has a Show instance. A PR merged in servant 0.17 (#1238) specifically redacts the Authorization header in Show and exceptions to prevent credential leaks. Other headers are shown normally, but Authorization is redacted (e.g., shown as "REDACTED").
Citations:
- 1: https://hackage.haskell.org/package/servant-client-core-0.20.3.0/docs/Servant-Client-Core-ClientError.html
- 2: https://hackage.haskell.org/package/servant-client-core-0.19/docs/Servant-Client-Core-ClientError.html
- 3: https://github.com/haskell-servant/servant/blob/master/servant-client-core/CHANGELOG.md
- 4: https://hackage.haskell.org/package/servant-client-core-0.20.3.0/docs/src/Servant.Client.Core.ClientError.html
- 5: https://hackage.haskell.org/package/servant-client-core-0.20.2/docs/Servant-Client-Core-Request.html
- 6: http://hackage.haskell.org/package/servant-client-core-0.20/docs/Servant-Client-Core-Request.html
- 7: https://hackage.haskell.org/package/servant-client-core-0.20.3.0/docs/Servant-Client-Core-Request.html
- 8: servant-client-core: Redact Authorization header haskell-servant/servant#1238
🏁 Script executed:
# Find dependency files to check the servant version
find . -type f \( -name "*.cabal" -o -name "stack.yaml" -o -name "cabal.project" -o -name "package.yaml" \) | head -20Repository: nammayatri/shared-kernel
Length of output: 249
🏁 Script executed:
# Read the file around line 49 to see the full context
cat -n lib/mobility-core/src/Kernel/External/Moengage/Moengage/Flow.hs | sed -n '40,60p'Repository: nammayatri/shared-kernel
Length of output: 777
🏁 Script executed:
# Check the servant version in the project dependencies
cat lib/mobility-core/package.yaml | grep -A 5 "^dependencies:" | head -20Repository: nammayatri/shared-kernel
Length of output: 156
🏁 Script executed:
# Also check if there's a specific servant-client dependency
rg "servant-client|servant-core" lib/mobility-core/package.yamlRepository: nammayatri/shared-kernel
Length of output: 110
🏁 Script executed:
# Read the full package.yaml to see version constraints
cat lib/mobility-core/package.yaml | grep -A 100 "^dependencies:" | grep -E "(servant|version)"Repository: nammayatri/shared-kernel
Length of output: 305
🏁 Script executed:
# Also check cabal.project for any version pins
cat cabal.projectRepository: nammayatri/shared-kernel
Length of output: 97
🏁 Script executed:
# Check if there's a stack.yaml file with version info
cat stack.yaml 2>/dev/null | head -50Repository: nammayatri/shared-kernel
Length of output: 50
🏁 Script executed:
# Check the cabal.project file for version constraints
cat cabal.project | grep -E "servant|constraints"Repository: nammayatri/shared-kernel
Length of output: 50
🏁 Script executed:
# Look for version constraints in the cabal file directly
cat lib/mobility-core/mobility-core.cabal | grep -A 100 "^build-depends:" | grep -E "servant" | head -10Repository: nammayatri/shared-kernel
Length of output: 50
🏁 Script executed:
# Look at the full package.yaml to see all dependencies with versions
cat lib/mobility-core/package.yaml | grep -E "^[a-z]|version:" | head -50Repository: nammayatri/shared-kernel
Length of output: 443
🏁 Script executed:
# Check the actual cabal.project file more carefully
cat cabal.projectRepository: nammayatri/shared-kernel
Length of output: 97
🏁 Script executed:
# Search for version bounds in package.yaml more comprehensively
rg "servant.*[<>=]|^ghc-version" lib/mobility-core/package.yamlRepository: nammayatri/shared-kernel
Length of output: 50
🏁 Script executed:
# Look for any constraint or freeze files
find . -type f \( -name "*.freeze" -o -name "*.lock" -o -name "cabal.freeze" \) | head -10Repository: nammayatri/shared-kernel
Length of output: 80
🏁 Script executed:
# Check if there's a hie.yaml or other config files that might specify versions
ls -la | grep -E "hie|stack|cabal|freeze"Repository: nammayatri/shared-kernel
Length of output: 190
🏁 Script executed:
# Look for resolver specification or GHC version
rg "resolver|ghc-version" . -t yaml -t toml --max-count 10Repository: nammayatri/shared-kernel
Length of output: 50
🏁 Script executed:
# Check the flake.lock for servant versions
cat flake.lock | grep -A 5 "servant-client\|servant-core" | head -50Repository: nammayatri/shared-kernel
Length of output: 50
🏁 Script executed:
# Get a full view of what's in flake.lock
cat flake.lock | head -100Repository: nammayatri/shared-kernel
Length of output: 2711
🏁 Script executed:
# Try a different approach - look for any nix files
find . -name "*.nix" -o -name "flake.nix" | head -10Repository: nammayatri/shared-kernel
Length of output: 79
🏁 Script executed:
# Check the flake.nix file for servant and GHC versions
cat flake.nix | grep -A 5 -B 5 "servant\|ghc-version\|haskellPackages"Repository: nammayatri/shared-kernel
Length of output: 50
🏁 Script executed:
# Get a better sense of the structure - look for haskellPackages or nixpkgs references
cat flake.nix | head -100Repository: nammayatri/shared-kernel
Length of output: 2650
🏁 Script executed:
# Check if there's any mention of GHC version in flake.nix
cat flake.nix | grep -i "ghc\|haskell"Repository: nammayatri/shared-kernel
Length of output: 811
🏁 Script executed:
# Look for any Haskell.nix or similar files that might specify versions
find . -name "*.nix" -type f | xargs grep -l "servant" 2>/dev/null | head -5Repository: nammayatri/shared-kernel
Length of output: 50
🏁 Script executed:
# Let's examine the actual ClientError type to understand what show would output
rg "ClientError" lib/mobility-core/src/Kernel/External/Moengage/Moengage/Flow.hs -B 2 -A 2Repository: nammayatri/shared-kernel
Length of output: 50
🏁 Script executed:
# Look at the imports in Flow.hs to understand what ClientError type is being used
head -50 lib/mobility-core/src/Kernel/External/Moengage/Moengage/Flow.hsRepository: nammayatri/shared-kernel
Length of output: 1902
🏁 Script executed:
# Search for callAPI function definition to understand what errors it might return
rg "callAPI" lib/mobility-core/src/Kernel/External/Moengage/Moengage/ -A 10 | head -50Repository: nammayatri/shared-kernel
Length of output: 818
🏁 Script executed:
# Look for callAPI in the broader codebase
rg "callAPI.*=" lib/mobility-core/src/ -A 3 | head -40Repository: nammayatri/shared-kernel
Length of output: 4678
🏁 Script executed:
# Find where callAPI is defined
rg "^callAPI\s*::" lib/mobility-core/src/ -A 5 | head -40Repository: nammayatri/shared-kernel
Length of output: 993
🏁 Script executed:
# Also search for callAPI definition
rg "callAPI\s+::" lib/mobility-core/src/ -B 2 -A 10 | head -60Repository: nammayatri/shared-kernel
Length of output: 2162
🏁 Script executed:
# Search for the callAPI function signature more broadly
rg "callAPI.*::" lib/mobility-core/src/Kernel/ -A 5 | head -50Repository: nammayatri/shared-kernel
Length of output: 4872
🏁 Script executed:
# Read the full callAPI' implementation to understand error handling
cat lib/mobility-core/src/Kernel/Utils/Servant/Client.hs | sed -n '100,200p'Repository: nammayatri/shared-kernel
Length of output: 4186
🏁 Script executed:
# Look for the complete callAPI' function
rg "callAPI'" lib/mobility-core/src/Kernel/Utils/Servant/Client.hs -A 30 | head -80Repository: nammayatri/shared-kernel
Length of output: 3398
🏁 Script executed:
# Check if there are similar patterns in the codebase where errors are handled differently
rg "fromEitherM.*show err" lib/mobility-core/src/ | head -20Repository: nammayatri/shared-kernel
Length of output: 3269
🏁 Script executed:
# Look for other error handling patterns to see if const is used elsewhere
rg "fromEitherM.*const.*InternalError" lib/mobility-core/src/ | head -20Repository: nammayatri/shared-kernel
Length of output: 50
🏁 Script executed:
# Check how other external API flows handle ClientError
rg ">>= fromEitherM" lib/mobility-core/src/Kernel/External/ -A 1 | head -40Repository: nammayatri/shared-kernel
Length of output: 3117
Sanitize low-level error details from external API failures.
Line 49 exposes raw ClientError details via show err. While servant-client automatically redacts Authorization headers since v0.17, the show instance still includes other sensitive information (HTTP status codes, response bodies, URLs). For security hardening, replace with:
- callAPI cfg.baseUrl (moengageClient cfg.appId cfg.appId (Just authToken) req) "moengageEvent" moengageEventAPI
- >>= fromEitherM (\err -> InternalError $ "Failed to call Moengage Event API: " <> show err)
+ callAPI cfg.baseUrl (moengageClient cfg.appId cfg.appId (Just authToken) req) "moengageEvent" moengageEventAPI
+ >>= fromEitherM (const $ InternalError "Failed to call Moengage Event API")Note: This pattern appears across multiple external API integrations (Wallet, Ticket, Tokenize, Insurance, Payment, Payout, Maps) and should be addressed consistently across the codebase.
📝 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.
| >>= fromEitherM (\err -> InternalError $ "Failed to call Moengage Event API: " <> show err) | |
| >>= fromEitherM (const $ InternalError "Failed to call Moengage Event API") |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/mobility-core/src/Kernel/External/Moengage/Moengage/Flow.hs` at line 49,
The current error mapping in Flow.hs uses fromEitherM (\err -> InternalError $
"Failed to call Moengage Event API: " <> show err), exposing raw ClientError
details; change it to return a generic, non-sensitive InternalError message
(e.g. "Failed to call Moengage Event API") without using show on the error, and
if needed capture/redact specific safe metadata in a separate audit/log path
(never inline the full show err). Update the mapping that produces InternalError
(the lambda passed to fromEitherM) to omit sensitive details and apply the same
replacement pattern to other external integrations (Wallet, Ticket, Tokenize,
Insurance, Payment, Payout, Maps) where fromEitherM or similar
error-to-InternalError conversions use show on low-level errors.
Add Moengage S2S API integration following the existing Kernel.External service pattern. Includes service enum, encrypted config, Servant API client, and pushEvent flow with Basic auth.
688e5fa to
07114d6
Compare
Summary
Kernel.External.Moengagemodule following the existing service integration pattern (similar to SOS, Insurance, Settlement)MoengageServiceenum,MoengageCfgwith encrypted API secret, Servant API client, andpushEventflow for Moengage S2S Event APIFiles Added
Kernel/External/Moengage.hs— Re-export moduleKernel/External/Moengage/Types.hs—MoengageServiceenumKernel/External/Moengage/Interface.hs— Provider dispatchKernel/External/Moengage/Interface/Types.hs—MoengageServiceConfigsum typeKernel/External/Moengage/Moengage/Config.hs—MoengageCfgwithEncryptedFieldKernel/External/Moengage/Moengage/Types.hs— API request/response typesKernel/External/Moengage/Moengage/API.hs— Servant API definitionKernel/External/Moengage/Moengage/Flow.hs—pushEventwith Basic authTest plan
cabal build mobility-corecompiles successfullyMoengageServiceConfigroundtrips correctly🤖 Generated with Claude Code
Summary by CodeRabbit
New Features