Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ When running againts multiple destination organizations, a seperate working dire
| roles | Sync Datadog roles. |
| rum_applications | Sync Datadog RUM applications. |
| rum_metrics | Sync Datadog RUM-based metrics. |
| rum_permanent_retention_filters | Sync Datadog permanent RUM retention filters (configure-only). |
| rum_retention_filters | Sync Datadog RUM retention filters (generic + exclusion). |
| rum_retention_filters_order | Sync Datadog RUM retention filters order. |
| sensitive_data_scanner_groups | Sync SDS groups |
Expand Down Expand Up @@ -365,6 +366,7 @@ See [Supported resources](#supported-resources) section below for potential reso
| roles | - |
| rum_applications | - |
| rum_metrics | - |
| rum_permanent_retention_filters | rum_applications |
| rum_retention_filters | rum_applications |
| rum_retention_filters_order | rum_applications, rum_retention_filters |
| sensitive_data_scanner_groups | - |
Expand Down
158 changes: 158 additions & 0 deletions datadog_sync/model/rum_permanent_retention_filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# Unless explicitly stated otherwise all files in this repository are licensed
# under the 3-clause BSD style license (see LICENSE).
# This product includes software developed at Datadog (https://www.datadoghq.com/).
# Copyright 2019 Datadog, Inc.

from __future__ import annotations
from typing import TYPE_CHECKING, Optional, List, Dict, Tuple

from datadog_sync.utils.base_resource import BaseResource, ResourceConfig

if TYPE_CHECKING:
from datadog_sync.utils.custom_client import CustomClient


class RUMPermanentRetentionFilters(BaseResource):
"""Permanent RUM retention filters (configure-only).

Permanent retention filters are system-defined with fixed ids
(``rum_apm_flat_sampling``, ``synthetics_sessions``, ``forced_replay_sessions``)
identical across orgs, so the filter id needs no remapping — only the parent
application id does. The endpoint set is PATCH-only (no POST/DELETE), so
``create_resource`` delegates to ``update_resource`` and ``delete_resource``
is a no-op, mirroring ``logs_archives_order``.

Because the filter ids are fixed and repeat under every application, the
state key is a **composite** ``"{application_id}:{filter_id}"`` to avoid
collisions when multiple applications are synced. The payload ``data.id``
remains the server filter id.

Like ``rum_retention_filters``, the application id is not part of the filter
body, so a synthetic ``_application_id`` is injected during enumeration and
remapped via ``resource_connections``. It is kept out of ``excluded_attributes``
(must survive ``prep_resource``) and is intentionally kept IN the diff so a
changed parent mapping (e.g. destination app deleted and recreated with a
new id) forces a PATCH rather than silently skipping it.
"""

resource_type = "rum_permanent_retention_filters"
resource_config = ResourceConfig(
base_path="/api/v2/rum/applications",
excluded_attributes=[
"attributes.name",
"attributes.description",
# NOTE: attributes.editability is deliberately NOT excluded here.
# update_resource needs to read editability.trace_editable to decide
# whether to strip trace fields from the PATCH body. If excluded,
# prep_resource strips it before update_resource runs, and the
# trace fields are never stripped (causing a 400 on
# rum_apm_flat_sampling where trace_editable=false).
],
resource_connections={
"rum_applications": ["_application_id"],
},
deep_diff_config={
"ignore_order": True,
# editability is read-only (returned by the API but not updatable).
# It must survive prep_resource (so update_resource can read
# trace_editable), but should not participate in diffs.
"exclude_regex_paths": [r".*\['editability'\]"],
},
Comment thread
Copilot marked this conversation as resolved.
skip_resource_mapping=True,
)
# Additional RUMPermanentRetentionFilters specific attributes
_applications_path = "/api/v2/rum/applications"

@staticmethod
def _composite_key(application_id: str, filter_id: str) -> str:
return f"{application_id}:{filter_id}"

async def get_resources(self, client: CustomClient) -> List[Dict]:
apps = (await client.get(self._applications_path))["data"]
resources: List[Dict] = []
for app in apps:
app_id = app["id"]
resp = await client.get(f"{self._applications_path}/{app_id}/retention_filters/permanent")
for f in resp["data"]:
f["_application_id"] = app_id
resources.append(f)
return resources

async def import_resource(self, _id: Optional[str] = None, resource: Optional[Dict] = None) -> Tuple[str, Dict]:
if _id:
# The {permanent_rf_id} GET is parent-scoped; search apps for it.
# Only used by --id-file (not allowlisted for this type).
# _id may be a composite key "{app_id}:{filter_id}" or just a
# filter id (legacy). When it's a composite key, look up directly.
source_client = self.config.source_client
if ":" in _id:
app_id, filter_id = _id.split(":", 1)
resp = await source_client.get(
f"{self._applications_path}/{app_id}/retention_filters/permanent/{filter_id}"
)
resource = resp["data"]
resource["_application_id"] = app_id
else:
apps = (await source_client.get(self._applications_path))["data"]
resource = None
for app in apps:
app_id = app["id"]
resp = await source_client.get(f"{self._applications_path}/{app_id}/retention_filters/permanent")
for f in resp["data"]:
if f["id"] == _id:
f["_application_id"] = app_id
resource = f
break
if resource:
break
if resource is None:
raise Exception(f"rum_permanent_retention_filter {_id} not found in any application")

resource = resource # type: ignore[assignment]
return self._composite_key(resource["_application_id"], resource["id"]), resource

async def pre_resource_action_hook(self, _id, resource: Dict) -> None:
pass

async def pre_apply_hook(self) -> None:
pass

async def create_resource(self, _id: str, resource: Dict) -> Tuple[str, Dict]:
# No POST endpoint — permanent filters are system-provisioned with fixed
# ids. Delegate to update (configure the cross_product_sampling).
return await self.update_resource(_id, resource)

async def update_resource(self, _id: str, resource: Dict) -> Tuple[str, Dict]:
destination_client = self.config.destination_client
app_id = resource.pop("_application_id", None)
# permanent filter ids are fixed across orgs; the filter id in the
# resource IS the destination filter id. The app_id was remapped by
# connect_resources to the destination app id.
filter_id = resource["id"]

# Respect the editability.trace_editable flag: when false, the API
# rejects PATCHes that include cross_product_sampling.trace_sample_rate
# or trace_enabled (400 'trace fields are not editable'). Strip those
# fields from the PATCH body so only non-trace attributes are sent.
editability = resource.get("attributes", {}).get("editability", {})
if not editability.get("trace_editable", True):
cps = resource.get("attributes", {}).get("cross_product_sampling", {})
if cps:
cps.pop("trace_sample_rate", None)
cps.pop("trace_enabled", None)
if not cps:
resource["attributes"].pop("cross_product_sampling", None)

payload = {"data": resource}
resp = await destination_client.patch(
f"{self._applications_path}/{app_id}/retention_filters/permanent/{filter_id}",
payload,
)
data = resp["data"]
data["_application_id"] = app_id
return _id, data

async def delete_resource(self, _id: str) -> None:
self.config.logger.warning(
"rum_permanent_retention_filters cannot be deleted. Removing resource from state only."
)
1 change: 1 addition & 0 deletions datadog_sync/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from datadog_sync.model.roles import Roles
from datadog_sync.model.rum_applications import RUMApplications
from datadog_sync.model.rum_metrics import RUMMetrics
from datadog_sync.model.rum_permanent_retention_filters import RUMPermanentRetentionFilters
from datadog_sync.model.rum_retention_filters import RUMRetentionFilters
from datadog_sync.model.rum_retention_filters_order import RUMRetentionFiltersOrder
from datadog_sync.model.security_monitoring_rules import SecurityMonitoringRules
Expand Down
Loading
Loading