Skip to content
Closed
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
46 changes: 41 additions & 5 deletions src/session_ops/zendesk/triage.py
Original file line number Diff line number Diff line change
Expand Up @@ -670,6 +670,9 @@ def analyze(model, effort, compact_tickets):
# Character budget for the links on that line, so a day of nothing but abuse reports
# can't push it past MAX_MESSAGE_CHARS. Past it the rest are counted, not linked.
COLLAPSED_LINKS_CHARS = 900
# Every other ticket the model did not flag goes on one line under it, linked and
# grouped by category: counted only in the tally, a ticket left the digest unseen.
UNFLAGGED_MARKER = "💤"

# ---- Components V2 ---------------------------------------------------------
#
Expand Down Expand Up @@ -756,6 +759,36 @@ def build_collapsed_line(collapsed, subdomain):
f"actionable ({', '.join(links)})")


def build_unflagged_line(unflagged, subdomain):
"""The tickets nobody is asked to look at, as one line under the abuse reports:

💤 **5** not flagged — 🔑 #28891, #28880 · ❓ #28870, #28866 · 💡 #28900

Grouped by category, most common first, so the emoji says what each run is
without a summary per ticket. Links share COLLAPSED_LINKS_CHARS the same way, and
the count always covers every one of them.
"""
by_category = {}
for f in unflagged:
by_category.setdefault(f.get("category"), []).append(f)
groups, used, unlisted = [], 0, 0
for category, members in sorted(by_category.items(), key=lambda kv: -len(kv[1])):
links = []
for f in members:
link = f"[#{f.get('id')}]({ticket_url(subdomain, f.get('id'))})"
used += len(link) + 2
if used > COLLAPSED_LINKS_CHARS:
unlisted += 1
continue
links.append(link)
if links:
groups.append(f"{CATEGORY_EMOJI.get(category, '•')} {', '.join(links)}")
if unlisted:
groups.append(f"+{unlisted} more")
return (f"{UNFLAGGED_MARKER} **{len(unflagged)}** not flagged — "
+ " · ".join(groups))


def build_header(findings, highlights, stats=None):
"""The lead lines: what was looked at, the category tally, duplicate clusters.

Expand Down Expand Up @@ -861,13 +894,14 @@ def build_messages(findings, subdomain, stats=None, updated_ids=None):
omitted_ids = {f.get("id") for f in omitted}
collapsed = [f for f in findings if is_collapsed(f)]
collapsed_ids = {f.get("id") for f in collapsed}
unflagged = [f for f in findings if f.get("id") not in shown_ids | omitted_ids | collapsed_ids]
unflagged_ids = {f.get("id") for f in unflagged}

# The header accounts for every classified ticket except the highlights that
# didn't fit; those are covered by no message and stay eligible next run. The
# collapsed line accounts for the abuse reports, links or not — its count covers
# every one of them, so re-reporting them next run would be noise.
# The highlights that didn't fit are covered by no message and stay eligible next
# run. The two summary lines account for every ticket they count, links or not,
# so re-reporting those next run would be noise.
header_ids = ({f.get("id") for f in findings}
- shown_ids - omitted_ids - collapsed_ids)
- shown_ids - omitted_ids - collapsed_ids - unflagged_ids)
header = build_header(findings, shown + omitted, stats)
if omitted:
header += (f"\nShowing the top **{len(shown)}** of "
Expand All @@ -880,6 +914,8 @@ def build_messages(findings, subdomain, stats=None, updated_ids=None):
# Last, under the individual tickets: lowest position for the lowest priority.
if collapsed:
entries.append((build_collapsed_line(collapsed, subdomain), collapsed_ids))
if unflagged:
entries.append((build_unflagged_line(unflagged, subdomain), unflagged_ids))

messages, coverage = discord.messages_from_entries(
header, entries, MAX_ENTRIES_PER_MESSAGE, MAX_COMPONENT_CHARS)
Expand Down
60 changes: 58 additions & 2 deletions tests/zendesk/test_triage.py
Original file line number Diff line number Diff line change
Expand Up @@ -558,8 +558,8 @@ class TestBuildMessages(unittest.TestCase):
def test_only_tickets_worth_looking_into_get_a_line(self):
findings = [finding(1), finding(2, worth_looking_into=False)]
text = digest_text(build_messages(findings, "acme"))
self.assertEqual(sum(1 for line in text.splitlines() if "[#" in line), 1)
self.assertIn("[#1]", text)
self.assertIn("| [#1]", text)
self.assertNotIn("| [#2]", text)

def test_highlights_are_ordered_by_priority_rank(self):
findings = [finding(1, priority_rank=3), finding(2, priority_rank=1)]
Expand Down Expand Up @@ -714,6 +714,62 @@ def test_nothing_is_clipped_when_it_fits(self):
self.assertNotIn("more", line)


class TestUnflaggedLine(unittest.TestCase):
"""Every ticket the model did not flag is still linked once, so a lost recovery
phrase the model judged routine can be found from the digest."""

def quiet(self, ticket_id, category="account_access"):
return finding(ticket_id, category=category, worth_looking_into=False)

def test_an_unflagged_ticket_is_linked(self):
text = digest_text(build_messages([finding(1), self.quiet(2)], "acme"))
self.assertIn("[#2](https://acme.zendesk.com/agent/tickets/2)", text)
self.assertIn("**1** not flagged", text)

def test_it_sits_below_the_abuse_reports(self):
findings = [self.quiet(2), finding(3, category="abuse_report",
severity="not_applicable"), finding(1)]
lines = [l for l in digest_text(build_messages(findings, "acme")).splitlines()
if "[#" in l]
self.assertIn("abuse report", lines[-2])
self.assertIn("not flagged", lines[-1])

def test_tickets_are_grouped_by_category_most_common_first(self):
findings = [self.quiet(1, "question"), self.quiet(2, "account_access"),
self.quiet(3, "account_access")]
line = triage.build_unflagged_line(findings, "acme")
recovery, question = (triage.CATEGORY_EMOJI[c] for c in ("account_access", "question"))
self.assertLess(line.index(recovery), line.index(question))
self.assertEqual(line.count(recovery), 1)

def test_abuse_reports_and_highlights_are_not_repeated_on_it(self):
findings = [finding(1), finding(2, category="abuse_report", severity="not_applicable"),
self.quiet(3)]
line = [l for l in digest_text(build_messages(findings, "acme")).splitlines()
if "not flagged" in l][0]
self.assertNotIn("[#1]", line)
self.assertNotIn("[#2]", line)
self.assertIn("**1** not flagged", line)

def test_highlights_beyond_the_cap_stay_off_it(self):
"""Those stay eligible for the next run rather than being marked seen."""
findings = [finding(i, priority_rank=i) for i in range(triage.MAX_HIGHLIGHTS + 2)]
self.assertNotIn("not flagged", digest_text(build_messages(findings, "acme")))

def test_it_covers_the_tickets_it_counts(self):
messages, coverage = triage.build_messages([finding(1), self.quiet(2)], "acme")
self.assertIn(2, set().union(*coverage))

def test_a_long_run_of_links_is_clipped_but_fully_counted(self):
line = triage.build_unflagged_line([self.quiet(27000 + i) for i in range(200)], "acme")
self.assertLess(len(line), triage.MAX_MESSAGE_CHARS)
self.assertIn("**200** not flagged", line)
self.assertRegex(line, r"\+\d+ more$")

def test_no_line_when_every_ticket_is_flagged(self):
self.assertNotIn("not flagged", digest_text(build_messages([finding(1)], "acme")))


# ---- Parsing helpers -------------------------------------------------------


Expand Down
Loading