Skip to content

fix: deep-merge range fields during alert update#297

Open
0xlaveen wants to merge 3 commits intomainfrom
fix/alert-update-range-merge
Open

fix: deep-merge range fields during alert update#297
0xlaveen wants to merge 3 commits intomainfrom
fix/alert-update-range-merge

Conversation

@0xlaveen
Copy link
Contributor

@0xlaveen 0xlaveen commented Mar 13, 2026

Summary

Original issue: Updating an alert with only --inflow-1h-min would drop the sibling max because the update handler did a shallow merge that replaced entire range objects.

Initial fix (commit f524acc): Special-case merge logic for inclusion and exclusion objects to preserve siblings.

Improvement (commit 3721c0a): Generalized solution with two key changes:

  1. Sparse ranges: buildRange now omits unsupplied ends instead of setting them to null. Eliminates the ambiguous null sentinel that caused the bug.
  2. Recursive merge: Replace the one-level merge loop with deepMergePlain(), which recursively deep-merges nested plain objects at any depth. This fixes the bug for ranges nested inside inclusion (like marketCap, fdvUsd, tokenAge), which the initial shallow merge still broke.

Scope

  • Affects all range fields on all 3 alert types
  • Top-level: inflow_*, outflow_*, netflow_*, usdValue, tokenAmount
  • Nested in inclusion: marketCap, fdvUsd, tokenAge

Test Coverage

  • Updated existing tests for sparse range objects
  • Top-level range merge (sm-token-flows inflow ranges)
  • Top-level range merge (common-token-transfer usdValue)
  • New: Nested range merge (inclusion.marketCap)
  • New: Nested range merge (inclusion.fdvUsd)
  • All 1035 tests pass ✅

…min/max

The shallow merge in the update handler replaced entire range objects
(e.g. inflow_1h, usdValue), so updating only --inflow-1h-min would
drop the existing max to null. Now all nested plain objects (ranges,
inclusion, exclusion) are deep-merged, and null sub-keys from
buildRange are preserved from the existing value.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@nansen-pr-reviewer
Copy link

pr-reviewer Summary

No issues found

The code review completed successfully with no findings.

Review effort: 2/5 (Simple)

Summary

This PR fixes a real bug where partially updating a range field (e.g., --inflow-1h-min only) would silently drop the sibling bound (max) to null, because buildRange emits { min: X, max: null } when only one side is specified and the prior shallow merge replaced the entire range object.

The fix is correct and well-reasoned: after the initial shallow merge, it iterates only the keys that buildAlertData produced (i.e., what the user actually supplied), detects nested plain objects, deep-merges them, and then restores any null sub-keys from the existing value — exactly matching the semantics of buildRange's null sentinel. Array fields (chains, subjects, etc.) are correctly excluded from deep-merging. The previous explicit special-casing of inclusion/exclusion is subsumed by the generalised loop with no regression in behaviour.

Two targeted tests are added covering the two alert types most likely to exercise range fields (sm-token-flows and common-token-transfer), including verification that untouched range siblings are also preserved end-to-end. The fix is minimal, self-contained, and consistent with the codebase's ESM/no-TypeScript conventions.


Token usage: 2,244 input, 4,779 output, 110,257 cache read, 17,509 cache write

Comment on lines +581 to +583
if (m[subKey] === null && oldVal[subKey] != null) {
m[subKey] = oldVal[subKey];
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Just something worth noting: This does prevent accidentally dropping the sibling bound but it also makes it impossible to explicitly clear a range bound. The API seems to accept null as a value to not include it in the range bound so I'm thinking we need to change when to apply the options.data to after the deep-merge loop, so it lands directly into params.data without being touched by the null-preservation logic.

- buildRange now omits keys for unsupplied values instead of setting null,
  eliminating the ambiguous null sentinel that caused the original bug.
- Replace one-level merge loop with recursive deepMergePlain(), which
  correctly handles ranges nested inside inclusion (marketCap, fdvUsd,
  tokenAge) — previously these were still silently dropped.
- Add test cases for inclusion-nested marketCap and fdvUsd merges.
- Update existing tests to expect sparse range objects.
@TimNooren TimNooren force-pushed the fix/alert-update-range-merge branch from b6ace4b to e4b044e Compare March 16, 2026 13:34
@TimNooren TimNooren closed this Mar 16, 2026
@TimNooren TimNooren reopened this Mar 16, 2026
The change in the original PR (f524acc) to always set subjects: [] broke
the update path: updating without --subject now wipes existing subjects.

Restore the conditional assignment (if (subjects) data.subjects = subjects;)
from main branch. This preserves the deep-merge behavior while keeping
the create path intact (TYPE_DEFAULTS still applies subjects: []).

Also restore the omitted test 'should not wipe subjects when updating
common-token-transfer without --subject' with updated assertion for
sparse range format (usdValue: { min: 5000 } not { min: 5000, max: null }).
@TimNooren TimNooren force-pushed the fix/alert-update-range-merge branch from ee006b1 to 0dcba3b Compare March 16, 2026 13:38
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.

3 participants