feat(rum): add rum_operation_strong_links resource - #720
michael-richey wants to merge 7 commits into
Conversation
michael-richey
left a comment
There was a problem hiding this comment.
Strong implementation overall; I like the way the composite key behavior is encoded for update/delete and how tests validate status-only updates.
One non-blocking robustness suggestion:
pre_resource_action_hooksilently no-ops ifrum_operationssource state is missing, which can lead to create payloads lackingapplication_id/operation_nameand producing less-actionable API errors later.- Consider raising
SkipResource(or a clear exception) when required derived fields cannot be populated, so operators get an explicit dependency/error message.
No blocking issues in this diff.
936598e to
893649f
Compare
2e1c183 to
6dd8547
Compare
|
Good suggestion — fixed in |
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Pagination is incomplete, and read-only attribute differences can cause perpetual ineffective updates.
Get a fresh assessment by requesting another Copilot review.
Review effort: Balanced
Findings: 1
Open (2)
What changed in this PR
Adds synchronization support for RUM operation strong links and their parent-resource mappings.
Changes:
- Implements list, create, update, delete, and dependency remapping.
- Registers and documents the resource.
- Adds unit coverage for resource operations.
| File | Description |
|---|---|
datadog_sync/model/rum_operation_strong_links.py |
Implements the resource model. |
datadog_sync/models/__init__.py |
Registers the model. |
tests/unit/test_rum_operation_strong_links.py |
Adds unit tests. |
README.md |
Documents support and dependencies. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| resp = await client.get(self.resource_config.base_path) | ||
|
|
||
| return resp["data"] |
| deep_diff_config={ | ||
| "ignore_order": True, | ||
| # application_id and operation_name are create-only (not in the | ||
| # response), so a source-vs-destination diff would always flag them. | ||
| "exclude_regex_paths": [r".*\['application_id'\]", r".*\['operation_name'\]"], |
893649f to
381b14d
Compare
c279302 to
608bc96
Compare
381b14d to
2fbb6d5
Compare
|
Fixed in
|
608bc96 to
f73ec4c
Compare
2fbb6d5 to
d58b3b5
Compare
Integration Test Findings (EU1 → US5 sync)Ran a full integration test with a Import failure — 400 Bad RequestRoot causeThe Suggested fixThe async def get_resources(self, client: CustomClient) -> List[Dict]:
# Get all operations from source state
operations = self.config.state.source.get("rum_operations", {})
all_strong_links = []
for op_id in operations:
resp = await client.get(f"{self.resource_config.base_path}?operation_id={op_id}")
all_strong_links.extend(resp["data"])
return all_strong_linksAlternatively, if the API supports listing all strong links without filters in some environments, the model should handle the 400 gracefully and fall back to per-operation fetching. What was testedA strong link was successfully created in EU1 via |
|
Fixed in
Two new tests: one verifying per-operation iteration with |
Re-test Results (after fix
|
f73ec4c to
67daf21
Compare
4440f02 to
2498ccf
Compare
|
Fixed in Fix: for the source client (import), |
2498ccf to
0215115
Compare
Fuzz Test Finding:
|
|
Fixed in Fix: |
Re-test: Reconciliation fix works but introduces
|
| Scenario | Result | Notes |
|---|---|---|
| Create | ❌ FAIL | 50 of 80 fail with type mismatch (the 30 that succeed are new creates with correct type) |
| Update | ✅ PASS | Reconciliation + update works correctly |
| Skip | ✅ PASS | No diff detected |
| Delete | ✅ PASS | Resources deleted at destination |
Fuzz sync results
The fuzz sync (171 resources) now passes with 0 failures:
- 169 successes
- 2 skipped (orphaned dependencies — correct behavior)
- 0 failures
All special chars, duplicates, pagination stress, and 504 timeout handling work correctly.
|
Fixed in Fix: |
fbd95da to
3a336dd
Compare
111428f to
99755b9
Compare
3a336dd to
bcf4158
Compare
99755b9 to
456a32c
Compare
RUM operation strong links are keyed by the composite (operation_id, feature_id). The create payload requires application_id and operation_name which are NOT in the response, so pre_resource_action_hook derives them from the parent source operation (it runs before connect_resources remaps operation_id). Only status is updatable, so update sends only status and the composite key is read from state.destination. application_id and operation_name are create-only, excluded from diffs via deep_diff_config (must survive prep_resource so create can read them). - datadog_sync/model/rum_operation_strong_links.py (new) - datadog_sync/models/__init__.py -- register - tests/unit/test_rum_operation_strong_links.py (new) -- 7 unit tests - README.md -- add rum_operation_strong_links (depends on rum_operations, rum_applications) Integration tests + VCR cassettes deferred (require sandbox-org API access).
Per review: pre_resource_action_hook silently no-ops when rum_operations source state is missing, which can lead to create payloads lacking application_id/operation_name and producing less-actionable API errors later. Now raises SkipResource with an explicit message when the parent operation can't be found or operation_id is missing, so operators get an immediate, actionable error.
…rom diff Per review (two comments): 1. The list endpoint is paginated but get_resources only returned the first page. Switch to client.paginated_request with PaginationConfig so all strong links are imported. Test updated to verify paginated_request is used. 2. The diff included response attributes that update_resource cannot change (description, tags, feature_id, operation_id). If any differed while status was equal, the handler repeatedly scheduled an update but the PUT sends only status, creating a non-converging loop. Now all non-updatable response fields are excluded from the diff via deep_diff_config (keeping them in the resource for create, but excluding from comparison).
Per integration test findings (EU1 -> US5 sync): The strong_links list endpoint (GET /api/v2/rum/operations/strong_links) requires at least one of operation_id or feature_id as a query parameter. Without it, the API returns 400 'At least one of operation_id or feature_id must be provided'. The OpenAPI spec marks them optional, but the API enforces at least one. Fix: get_resources now iterates over rum_operations in state (source for import, destination for apply) and fetches strong links per operation_id using a query parameter. Falls back to an empty list when no operations are in state yet. Removed the unused PaginationConfig since per-operation fetches are unlikely to need pagination. Two new tests: one verifying per-operation iteration, one verifying empty result when no operations exist.
Per integration test re-test: get_resources used state.source['rum_operations'] to find operation IDs, but during import discovery all resource types run in parallel, so state isn't populated yet -> 0 strong links imported. Fix: for the source client (import), get_resources now fetches operations directly from the API search endpoint instead of relying on state. For the destination client (apply), it still uses state (rum_operations is already synced by then). Tests updated to verify both paths.
Per fuzz test findings: rum_operation_strong_links has skip_resource_mapping=True, so the pre-apply listing phase is skipped. When strong links already exist at the destination from a previous sync, create_resource POSTs unconditionally and gets 409 Conflict. Fix: create_resource now searches for an existing strong link with the same operation_id + feature_id at the destination before POSTing. If found, it hydrates state and delegates to update. New test verifies the reconciliation path.
…ayloads Per integration test re-test: create_resource sent the resource body as-is with type='rum_operation_strong_links' (the sync-cli resource type), but the API expects type='strong_links'. This caused 400 Bad Request on 50 of 80 strong links. update_resource had the same bug in its PUT payload. Fix: create_resource now sets resource['type'] = 'strong_links' before POSTing. update_resource uses 'strong_links' in the PUT payload type field instead of self.resource_type. Test updated to verify the correct API type.
bcf4158 to
dcf0952
Compare
456a32c to
258282d
Compare
|
Thanks for adding strong-link support — this is close, but I see two correctness issues to fix: 1)
|


Summary
RUM operation strong links — keyed by the composite (
operation_id,feature_id). Depends onrum_operations(andrum_applications).Stacked on #719 (
rum_operations).Design
application_idandoperation_namewhich are not in the response.pre_resource_action_hookderives them from the parent source operation (it runs beforeconnect_resourcesremapsoperation_id, so the operation_id is still the source id andstate.source["rum_operations"]is keyed by it).status(the sole updatable field); the composite key is read fromstate.destination.state.destination.application_idandoperation_nameare create-only (not in the response), so they're excluded from diffs viadeep_diff_config.exclude_regex_paths(must surviveprep_resourceso create can read them).resource_connections:rum_operations(remapattributes.operation_id),rum_applications(remapattributes.application_id).Changes
datadog_sync/model/rum_operation_strong_links.py(new)datadog_sync/models/__init__.py— registertests/unit/test_rum_operation_strong_links.py(new) — 7 unit testsREADME.md— addrum_operation_strong_links(depends onrum_operations,rum_applications)Testing
pytest tests/unit/test_rum_operation_strong_links.py→ 7 passedAssumptions (to verify during integration-test recording)
operation_namein the create payload maps to the parent operation'sattributes.name.status(perRUMOperationStrongLinkUpdateAttributes).Follow-up
Integration tests + VCR cassettes deferred (require sandbox-org API access).