diff --git a/conserver/links/post_analysis_to_slack/README.md b/conserver/links/post_analysis_to_slack/README.md index dbc8dbb..b99253b 100644 --- a/conserver/links/post_analysis_to_slack/README.md +++ b/conserver/links/post_analysis_to_slack/README.md @@ -18,7 +18,7 @@ The Post Analysis to Slack link is a specialized plugin that posts vCon analysis default_options = { "token": None, # Slack API token "channel_name": None, # Default Slack channel name - "url": "Url to hex sheet", # URL for the details button + "url": "Url to link to the details page", # URL template for the details button "analysis_to_post": "summary", # Type of analysis to post "only_if": { "analysis_type": "customer_frustration", # Analysis type to match @@ -31,12 +31,31 @@ default_options = { - `token`: Your Slack API token - `channel_name`: Default Slack channel for notifications -- `url`: URL for the details button in the Slack message +- `url`: URL template for the details button (see below) - `analysis_to_post`: Type of analysis to post (e.g., "summary") - `only_if`: Conditions that must be met for posting: - `analysis_type`: Type of analysis to match - `includes`: Text that must be included in the analysis body +### URL templating + +The `url` option is a template. If it contains `{vcon_id}`, the vCon uuid +is substituted at that exact position — the template owns the quoting and +placement. This lets the same link work with destinations that embed the +id in different shapes: + +```yaml +# Hex sheet — uuid must be wrapped in double quotes inside a query param. +url: 'https://app.hex.tech//app//latest?_vcon_id="{vcon_id}"' + +# Path-based destination — uuid embedded raw in the path. +url: "https://app.example.com/conversations/{vcon_id}" +``` + +If the `url` has no `{vcon_id}` placeholder, the link falls back to the +legacy Hex-shaped suffix (`?_vcon_id=""`) so existing configs keep +working unchanged. + ## Usage The link processes vCons by: diff --git a/conserver/links/post_analysis_to_slack/__init__.py b/conserver/links/post_analysis_to_slack/__init__.py index c2f0ee9..dccbf4b 100644 --- a/conserver/links/post_analysis_to_slack/__init__.py +++ b/conserver/links/post_analysis_to_slack/__init__.py @@ -8,12 +8,29 @@ default_options = { "token": None, "channel_name": None, - "url": "Url to hex sheet", + "url": "Url to link to the details page", "analysis_to_post": "summary", "only_if": {"analysis_type": "customer_frustration", "includes": "NEEDS REVIEW"}, } +def build_details_url(url_template: str, vcon_uuid: str) -> str: + """Render the details URL for a vCon. + + If ``url_template`` contains a ``{vcon_id}`` placeholder, substitute the + uuid into it — letting each deployment decide where and how the id is + embedded (e.g. quoted for Hex, raw in a path for the portal). + + Otherwise, fall back to the legacy ``?_vcon_id=""`` suffix so + existing configs keep working unchanged. + """ + if "{vcon_id}" in url_template: + # str.replace (not str.format) so unrelated ``{...}`` segments in + # the configured URL can't raise KeyError/ValueError at runtime. + return url_template.replace("{vcon_id}", vcon_uuid) + return f'{url_template}?_vcon_id="{vcon_uuid}"' + + def get_team(vcon): team_name = None for a in vcon.attachments: @@ -108,7 +125,7 @@ def run(vcon_id, link_name, opts=default_options): # and uses ``only_if.analysis_type`` + ``only_if.includes``. # Unifying the two is its own refactor. - url = f"{opts['url']}?_vcon_id=\"{vcon.uuid}\"" + url = build_details_url(opts["url"], vcon.uuid) team_name = get_team(vcon) dealer_name = get_dealer(vcon) summary = get_summary(vcon, a["dialog"]) diff --git a/conserver/links/post_analysis_to_slack/test_post_analysis_to_slack.py b/conserver/links/post_analysis_to_slack/test_post_analysis_to_slack.py index 7978e4a..0dd0ea3 100644 --- a/conserver/links/post_analysis_to_slack/test_post_analysis_to_slack.py +++ b/conserver/links/post_analysis_to_slack/test_post_analysis_to_slack.py @@ -4,6 +4,7 @@ import pytest from links.post_analysis_to_slack import ( + build_details_url, get_dealer, get_summary, get_team, @@ -48,6 +49,42 @@ def test_helper_functions_extract_team_dealer_and_summary(): assert get_summary(vcon, 1) is None +def test_build_details_url_legacy_appends_quoted_query_param(): + # Old config style with no placeholder — keep the legacy Hex-shaped suffix. + assert ( + build_details_url("https://details.test", "test-uuid") + == 'https://details.test?_vcon_id="test-uuid"' + ) + + +def test_build_details_url_hex_placeholder_quotes_uuid(): + # Hex still expects the uuid wrapped in double quotes; the template owns the quoting. + template = 'https://app.hex.tech/x/app/y/latest?_vcon_id="{vcon_id}"' + assert ( + build_details_url(template, "abc-123") + == 'https://app.hex.tech/x/app/y/latest?_vcon_id="abc-123"' + ) + + +def test_build_details_url_portal_placeholder_embeds_raw_uuid(): + # Portal embeds the uuid directly in the path, no quotes. + template = "https://portal.strolidcxm.com/app/conversations/{vcon_id}" + assert ( + build_details_url(template, "abc-123") + == "https://portal.strolidcxm.com/app/conversations/abc-123" + ) + + +def test_build_details_url_leaves_unrelated_braces_alone(): + # Unrelated ``{...}`` segments in the template must NOT raise — only + # ``{vcon_id}`` is substituted. + template = "https://app.example.com/users/{user_id}/conversations/{vcon_id}" + assert ( + build_details_url(template, "abc-123") + == "https://app.example.com/users/{user_id}/conversations/abc-123" + ) + + @patch("links.post_analysis_to_slack.increment_counter") @patch("links.post_analysis_to_slack.WebClient") def test_post_blocks_to_channel_falls_back_and_reraises(mock_web_client, mock_increment_counter):