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 @@ -259,6 +259,7 @@ When running againts multiple destination organizations, a seperate working dire
| rum_applications | Sync Datadog RUM applications. |
| rum_metrics | Sync Datadog RUM-based metrics. |
| rum_operations | Sync Datadog RUM operations. |
| rum_replay_playlists | Sync Datadog RUM replay playlists (shell only). |
| rum_operation_strong_links | Sync Datadog RUM operation strong links. |
| rum_permanent_retention_filters | Sync Datadog permanent RUM retention filters (configure-only). |
| rum_retention_filters | Sync Datadog RUM retention filters (generic + exclusion). |
Expand Down Expand Up @@ -369,6 +370,7 @@ See [Supported resources](#supported-resources) section below for potential reso
| rum_applications | - |
| rum_metrics | - |
| rum_operations | rum_applications |
| rum_replay_playlists | - |
| rum_operation_strong_links | rum_operations, rum_applications |
| rum_permanent_retention_filters | rum_applications |
| rum_retention_filters | rum_applications |
Expand Down
80 changes: 80 additions & 0 deletions datadog_sync/model/rum_replay_playlists.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# 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 RUMReplayPlaylists(BaseResource):
"""RUM replay playlists (shell only).

A RUM replay playlist is a static shell definition (name, description).
The playlist body itself contains no session-id references; the
playlist->session associations live on the ``.../sessions`` sub-resources,
which are intake-tied and out of scope. So this model syncs the playlist
shell only (full CRUD) and does not call the ``.../sessions`` endpoints.
"""

resource_type = "rum_replay_playlists"
resource_config = ResourceConfig(
base_path="/api/v2/rum/replay/playlists",
excluded_attributes=[
"id",
"attributes.created_at",
"attributes.updated_at",
"attributes.created_by",
"attributes.session_count",
],
skip_resource_mapping=True,
)
# Additional RUMReplayPlaylists specific attributes

async def get_resources(self, client: CustomClient) -> List[Dict]:
resp = await client.get(self.resource_config.base_path)

return resp["data"]

async def import_resource(self, _id: Optional[str] = None, resource: Optional[Dict] = None) -> Tuple[str, Dict]:
if _id:
source_client = self.config.source_client
resource = (await source_client.get(self.resource_config.base_path + f"/{_id}"))["data"]

return 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]:
destination_client = self.config.destination_client
# create data has no id (server-assigned)
resource.pop("id", None)
payload = {"data": resource}
resp = await destination_client.post(self.resource_config.base_path, payload)
return _id, resp["data"]

async def update_resource(self, _id: str, resource: Dict) -> Tuple[str, Dict]:
destination_client = self.config.destination_client
destination_id = self.config.state.destination[self.resource_type][_id]["id"]
resource["id"] = destination_id
payload = {"data": resource}
resp = await destination_client.put(
self.resource_config.base_path + f"/{destination_id}",
payload,
)
return _id, resp["data"]

async def delete_resource(self, _id: str) -> None:
destination_client = self.config.destination_client
await destination_client.delete(
self.resource_config.base_path + f"/{self.config.state.destination[self.resource_type][_id]['id']}"
)
1 change: 1 addition & 0 deletions datadog_sync/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from datadog_sync.model.rum_operations import RUMOperations
from datadog_sync.model.rum_operation_strong_links import RUMOperationStrongLinks
from datadog_sync.model.rum_permanent_retention_filters import RUMPermanentRetentionFilters
from datadog_sync.model.rum_replay_playlists import RUMReplayPlaylists
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
122 changes: 122 additions & 0 deletions tests/unit/test_rum_replay_playlists.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# 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.

"""
Unit tests for the RUMReplayPlaylists resource model.

A RUM replay playlist is a static shell definition (name, description). The
playlist body itself contains no session-id references; the playlist->session
associations live on the ``.../sessions`` sub-resources, which are intake-tied
and out of scope. So this model syncs the playlist shell only (full CRUD) and
does not call the ``.../sessions`` endpoints.
"""

import asyncio
from collections import defaultdict
from unittest.mock import AsyncMock, MagicMock

from datadog_sync.model.rum_replay_playlists import RUMReplayPlaylists


def _run(coro):
loop = asyncio.new_event_loop()
try:
return loop.run_until_complete(coro)
finally:
loop.close()


def _pl(_id, name="prod-replay"):
return {
"id": _id,
"type": "replay_playlists",
"attributes": {
"name": name,
"description": "prod replay playlist",
},
}


def test_get_resources_hits_list_endpoint():
pl = RUMReplayPlaylists(MagicMock())
client = AsyncMock()
client.get = AsyncMock(return_value={"data": [_pl("pl-1")]})

resources = _run(pl.get_resources(client))

assert resources == [_pl("pl-1")]
client.get.assert_awaited_once_with("/api/v2/rum/replay/playlists")


def test_import_resource_by_id_gets_and_returns():
pl = RUMReplayPlaylists(MagicMock())
source = AsyncMock()
source.get = AsyncMock(return_value={"data": _pl("pl-1")})
pl.config.source_client = source

_id, data = _run(pl.import_resource(_id="pl-1"))

assert _id == "pl-1"
source.get.assert_awaited_once_with("/api/v2/rum/replay/playlists/pl-1")


def test_import_resource_passthrough():
pl = RUMReplayPlaylists(MagicMock())
pl.config.source_client = AsyncMock()
resource = _pl("pl-1")
_id, data = _run(pl.import_resource(resource=resource))
assert _id == "pl-1"
assert data is resource


def test_create_resource_posts_without_id():
pl = RUMReplayPlaylists(MagicMock())
dest = AsyncMock()
dest.post = AsyncMock(return_value={"data": _pl("pl-dst")})
pl.config.destination_client = dest

resource = _pl("pl-1")
_id, data = _run(pl.create_resource("pl-1", resource))

assert _id == "pl-1"
assert data["id"] == "pl-dst"
assert "id" not in resource
dest.post.assert_awaited_once()
post_url, post_payload = dest.post.await_args.args
assert post_url == "/api/v2/rum/replay/playlists"
assert post_payload == {"data": resource}


def test_update_resource_puts_destination_id():
pl = RUMReplayPlaylists(MagicMock())
dest = AsyncMock()
dest.put = AsyncMock(return_value={"data": _pl("pl-dst", name="updated")})
pl.config.destination_client = dest
pl.config.state = MagicMock()
pl.config.state.destination = defaultdict(dict)
pl.config.state.destination["rum_replay_playlists"]["pl-1"] = {"id": "pl-dst"}

resource = _pl("pl-1", name="updated")
_id, data = _run(pl.update_resource("pl-1", resource))

assert _id == "pl-1"
assert resource["id"] == "pl-dst"
dest.put.assert_awaited_once()
put_url, put_payload = dest.put.await_args.args
assert put_url == "/api/v2/rum/replay/playlists/pl-dst"
assert put_payload == {"data": resource}


def test_delete_resource_deletes_destination_id():
pl = RUMReplayPlaylists(MagicMock())
dest = AsyncMock()
pl.config.destination_client = dest
pl.config.state = MagicMock()
pl.config.state.destination = defaultdict(dict)
pl.config.state.destination["rum_replay_playlists"]["pl-1"] = {"id": "pl-dst"}

_run(pl.delete_resource("pl-1"))

dest.delete.assert_awaited_once_with("/api/v2/rum/replay/playlists/pl-dst")
Loading