release: Merging develop to Production release#247
Conversation
Updated merge logic to handle add/replace oprations
Update DefaultAppEventPayloadHandler.java
📝 WalkthroughWalkthroughThe file refactors payload storage by replacing a single method with branched logic based on collection type. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 3❌ Failed checks (1 warning, 2 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 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 |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
app/src/main/java/org/curiouslearning/container/core/subapp/handler/DefaultAppEventPayloadHandler.java (1)
209-227: Keepoptionsaligned with the payload contract.
AppEventPayload.optionsis alreadyMap<String, String>, andAppEventPayloadValidatoronly allows"add"or"replace". Rebuilding it asMap<?, ?>→Map<String, Object>weakens that contract and silently turns malformed values into"replace"instead of failing fast. Reading it asMap<String, String>keeps the handler consistent with the validator.♻️ Suggested simplification
- Map<String, Object> options = new HashMap<>(); - - if (payload.options instanceof Map) { - Map<?, ?> raw = (Map<?, ?>) payload.options; - for (Map.Entry<?, ?> entry : raw.entrySet()) { - options.put(String.valueOf(entry.getKey()), entry.getValue()); - } - } + Map<String, String> options = + payload.options != null + ? payload.options + : java.util.Collections.emptyMap(); ... - String operation = - options.get(key) instanceof String - ? (String) options.get(key) - : "replace"; + String operation = options.getOrDefault(key, "replace");🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/org/curiouslearning/container/core/subapp/handler/DefaultAppEventPayloadHandler.java` around lines 209 - 227, The handler rebuilds payload.options into Map<String,Object>, weakening the contract; change the local options to Map<String,String> and read it directly from payload.options (e.g., Map<String,String> options = payload.options != null ? payload.options : Collections.emptyMap()), stop converting values to Object, and when computing operation (the operation variable used alongside newData and merged in DefaultAppEventPayloadHandler) retrieve the String via options.get(key) (with a null check) so you preserve the payload contract and rely on AppEventPayloadValidator's "add"/"replace" guarantees rather than silently coercing malformed values.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@app/src/main/java/org/curiouslearning/container/core/subapp/handler/DefaultAppEventPayloadHandler.java`:
- Around line 106-118: In storeSummaryPayload, add a fast-fail guard that
validates payload.data is a Map (and non-null) before performing the Firestore
lookup; if payload.data is missing or not an instance of Map, log a
warning/error and return immediately to avoid calling query.get() or writing
back an unchanged record (this prevents mergeData from being invoked on
malformed data). Locate the validation at the start of storeSummaryPayload (use
AppEventPayload.data and the mergeData call site as references) and ensure no DB
reads/writes occur when the guard rejects the payload.
- Around line 113-161: storeSummaryPayload performs a non-atomic query-get +
client-side merge (mergeData) followed by set/create (createNewSummaryDoc),
which is TOCTOU-prone and also treats query failures as "no record" causing
duplicates; fix by computing a deterministic document ID (e.g., derive from
payload.cr_user_id + payload.app_id + payload.collection) and then perform an
atomic write (use Firestore transaction or document().set with merge on that
deterministic ID) instead of query-first logic, move payload.data validation up
front to match storeUserSessionPayload before any database ops, and change the
error branch to fail/retry the operation rather than assume missing record.
Ensure storeSummaryPayload, mergeData, and createNewSummaryDoc are updated to
use the deterministic ID or transaction-based update.
---
Nitpick comments:
In
`@app/src/main/java/org/curiouslearning/container/core/subapp/handler/DefaultAppEventPayloadHandler.java`:
- Around line 209-227: The handler rebuilds payload.options into
Map<String,Object>, weakening the contract; change the local options to
Map<String,String> and read it directly from payload.options (e.g.,
Map<String,String> options = payload.options != null ? payload.options :
Collections.emptyMap()), stop converting values to Object, and when computing
operation (the operation variable used alongside newData and merged in
DefaultAppEventPayloadHandler) retrieve the String via options.get(key) (with a
null check) so you preserve the payload contract and rely on
AppEventPayloadValidator's "add"/"replace" guarantees rather than silently
coercing malformed values.
🪄 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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 567a7169-bc36-499b-afde-2b09cf4c3755
📒 Files selected for processing (1)
app/src/main/java/org/curiouslearning/container/core/subapp/handler/DefaultAppEventPayloadHandler.java
| private void storeSummaryPayload( | ||
| FirebaseFirestore db, | ||
| AppEventPayload payload | ||
| ) { | ||
|
|
||
| Log.d(TAG, "Querying summary record (offline-first)"); | ||
|
|
||
| Query query = db.collection(payload.collection) | ||
| .whereEqualTo("cr_user_id", payload.cr_user_id) | ||
| .whereEqualTo("app_id", payload.app_id) | ||
| .limit(1); | ||
|
|
||
| query.get() |
There was a problem hiding this comment.
Reject malformed summary_data before the lookup.
This path does not fail fast on non-Map payload.data. If a summary already exists, mergeData() logs the error but returns the previous map, and this method still writes the record back with a new timestamp. Invalid summary events should return before any read/write.
🛡️ Suggested guard
private void storeSummaryPayload(
FirebaseFirestore db,
AppEventPayload payload
) {
+ if (!(payload.data instanceof Map)) {
+ Log.e(TAG, "Invalid payload.data type. Expected Map but got: "
+ + (payload.data == null ? "null" : payload.data.getClass()));
+ return;
+ }
+
Log.d(TAG, "Querying summary record (offline-first)");🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@app/src/main/java/org/curiouslearning/container/core/subapp/handler/DefaultAppEventPayloadHandler.java`
around lines 106 - 118, In storeSummaryPayload, add a fast-fail guard that
validates payload.data is a Map (and non-null) before performing the Firestore
lookup; if payload.data is missing or not an instance of Map, log a
warning/error and return immediately to avoid calling query.get() or writing
back an unchanged record (this prevents mergeData from being invoked on
malformed data). Locate the validation at the start of storeSummaryPayload (use
AppEventPayload.data and the mergeData call site as references) and ensure no DB
reads/writes occur when the guard rejects the payload.
| Query query = db.collection(payload.collection) | ||
| .whereEqualTo("cr_user_id", payload.cr_user_id) | ||
| .whereEqualTo("app_id", payload.app_id) | ||
| .limit(1); | ||
|
|
||
| query.get() | ||
| .addOnSuccessListener(querySnapshot -> { | ||
|
|
||
| Map<String, Object> record = new HashMap<>(); | ||
| record.put("cr_user_id", payload.cr_user_id); | ||
| record.put("app_id", payload.app_id); | ||
| record.put("collection", payload.collection); | ||
| record.put("timestamp", payload.timestamp); | ||
|
|
||
| Map<String, Object> mergedData = | ||
| mergeData(existingDoc, payload); | ||
| if (!querySnapshot.isEmpty()) { | ||
|
|
||
| List<DocumentSnapshot> documents = querySnapshot.getDocuments(); | ||
| DocumentSnapshot existingDoc = documents.get(0); | ||
|
|
||
| Map<String, Object> mergedData = | ||
| mergeData(existingDoc, payload); | ||
|
|
||
| record.put("data", mergedData); | ||
| record.put("data", mergedData); | ||
|
|
||
| // ✅ Idempotent write | ||
| documentRef.set(record, SetOptions.merge()) | ||
| .addOnSuccessListener(aVoid -> | ||
| Log.d(TAG, "Summary payload upserted in Firestore")) | ||
| .addOnFailureListener(e -> | ||
| Log.e(TAG, "Failed to upsert summary payload", e)); | ||
| db.collection(payload.collection) | ||
| .document(existingDoc.getId()) | ||
| .set(record) | ||
| .addOnSuccessListener(aVoid -> | ||
| Log.d(TAG, "Updated summary payload")) | ||
| .addOnFailureListener(e -> | ||
| Log.e(TAG, "Failed to update summary payload", e)); | ||
|
|
||
| } else { | ||
|
|
||
| Log.d(TAG, "No existing summary record — creating new"); | ||
| createNewSummaryDoc(db, payload, record); | ||
| } | ||
| }) | ||
| .addOnFailureListener(e -> { | ||
|
|
||
| Log.w(TAG, "Query failed — creating new summary record", e); | ||
|
|
||
| Map<String, Object> record = new HashMap<>(); | ||
| record.put("cr_user_id", payload.cr_user_id); | ||
| record.put("app_id", payload.app_id); | ||
| record.put("collection", payload.collection); | ||
| record.put("timestamp", payload.timestamp); | ||
|
|
||
| createNewSummaryDoc(db, payload, record); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
fd -t f "DefaultAppEventPayloadHandler.java" --exec wc -l {}Repository: curiouslearning/CRcontainer
Length of output: 179
🏁 Script executed:
fd -t f "DefaultAppEventPayloadHandler.java" --exec cat -n {}Repository: curiouslearning/CRcontainer
Length of output: 10989
🏁 Script executed:
fd -t f "AppEventPayload.java" --exec wc -l {}Repository: curiouslearning/CRcontainer
Length of output: 164
🏁 Script executed:
fd -t f "AppEventPayload.java" --exec cat -n {}Repository: curiouslearning/CRcontainer
Length of output: 457
🏁 Script executed:
fd -t f "AppEventPayloadValidator.java" --exec cat -n {}Repository: curiouslearning/CRcontainer
Length of output: 1972
🏁 Script executed:
# Verify there are no other query-based upsert patterns in the codebase that might mitigate this
rg "query\.get\(\)" --type java -A 5Repository: curiouslearning/CRcontainer
Length of output: 956
Make the summary_data upsert atomic.
query.get() + client-side merge + separate set()/add() is a TOCTOU write. Concurrent events for the same cr_user_id/app_id can both read the same snapshot, merge independently, and then both write—causing the second to overwrite the first's merged changes. The query-failure branch (line 151) also incorrectly treats transient read errors as absence, creating duplicate documents. Use a deterministic document ID or a transactional update path instead of query-first upsert.
Additionally, storeSummaryPayload lacks upfront validation of payload.data before the query, unlike storeUserSessionPayload (lines 77–81). This allows malformed payloads to reach the merge logic.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@app/src/main/java/org/curiouslearning/container/core/subapp/handler/DefaultAppEventPayloadHandler.java`
around lines 113 - 161, storeSummaryPayload performs a non-atomic query-get +
client-side merge (mergeData) followed by set/create (createNewSummaryDoc),
which is TOCTOU-prone and also treats query failures as "no record" causing
duplicates; fix by computing a deterministic document ID (e.g., derive from
payload.cr_user_id + payload.app_id + payload.collection) and then perform an
atomic write (use Firestore transaction or document().set with merge on that
deterministic ID) instead of query-first logic, move payload.data validation up
front to match storeUserSessionPayload before any database ops, and change the
error branch to fail/retry the operation rather than assume missing record.
Ensure storeSummaryPayload, mergeData, and createNewSummaryDoc are updated to
use the deterministic ID or transaction-based update.
Changes
Ref: MR-45
Summary by CodeRabbit
Bug Fixes
Chores