Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
7b6713d
feat: Add Zexbox.OpenTelemetry URL helpers with tests
Jez-A Mar 9, 2026
6a1cdb9
feat: Add JiraClient and OpenTelemetry URL helpers
Jez-A Mar 3, 2026
9be32aa
style: Run mix format
Jez-A Mar 3, 2026
e89c213
fix: Use POST /search/jql for search and add Accept header to all req…
Jez-A Mar 3, 2026
fdb872b
refactor: Convert JiraClient public API to explicit positional arguments
Jez-A Mar 3, 2026
6d6528f
fix: Name bare _ wildcards to satisfy Credo consistency check
Jez-A Mar 3, 2026
8fac2a3
feat: Add AdfBuilder for Jira ADF document generation
Jez-A Mar 3, 2026
6d88a9e
style: Run mix format
Jez-A Mar 3, 2026
22cf890
feat: Remove Datadog session link from telemetry paragraph
Jez-A Mar 3, 2026
4ec7726
fix: Name bare _ wildcards to satisfy Credo consistency check
Jez-A Mar 9, 2026
4bdc42c
feat: Remove Jaeger fallback from generate_trace_url
Jez-A Mar 9, 2026
61b7154
feat: Add JiraClient and OpenTelemetry URL helpers
Jez-A Mar 3, 2026
5113876
style: Run mix format
Jez-A Mar 3, 2026
d474fe2
fix: Use POST /search/jql for search and add Accept header to all req…
Jez-A Mar 3, 2026
a488bbc
refactor: Convert JiraClient public API to explicit positional arguments
Jez-A Mar 3, 2026
0fe9c12
fix: Name bare _ wildcards to satisfy Credo consistency check
Jez-A Mar 3, 2026
34f4acb
Cleanup after moving the Telemetry to another PR
Jez-A Mar 9, 2026
4ed4e32
Cleanup mix too
Jez-A Mar 9, 2026
95a2974
not needed
Jez-A Mar 9, 2026
8d572e6
Corrected fields type and cleanup
Jez-A Mar 9, 2026
8d02399
Admin
Jez-A Mar 9, 2026
84bde06
feat: Add AdfBuilder for Jira ADF document generation
Jez-A Mar 3, 2026
205e991
style: Run mix format
Jez-A Mar 3, 2026
1e5ba25
feat: Remove Datadog session link from telemetry paragraph
Jez-A Mar 3, 2026
9fc0615
fix: Name bare _ wildcards to satisfy Credo consistency check
Jez-A Mar 9, 2026
761cacb
feat: Add Zexbox.JiraClient (#53)
Jez-A Mar 9, 2026
e8a673f
Merge branch 'master' into auto_escalation_adf_builder
Jez-A Mar 9, 2026
65c9771
Merge branch 'auto_escalation_adf_builder' of github.com:Intellection…
Jez-A Mar 9, 2026
9d66a3f
Conflict resolutions
Jez-A Mar 9, 2026
5bd4534
refactor: Convert AdfBuilder public API to explicit positional arguments
Jez-A Mar 10, 2026
1093029
style: Run mix format
Jez-A Mar 10, 2026
c18cbe7
refactor: Delegate AdfBuilder content assembly to append_* pipeline
Jez-A Mar 10, 2026
cbe3233
refactor: Rename ctx to context in append_single_context for consistency
Jez-A Mar 10, 2026
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## 1.5.2 - 2026-03-09

- Added the JiraClient, allowing us to create, search, comment on Jira tickets for support processes.

## 1.5.1 - 2026-02-05

- Handles cases where `$callers` and `$ancestors` may not be pids to avoid crashing metric handler.
Expand Down
195 changes: 195 additions & 0 deletions lib/zexbox/auto_escalation/adf_builder.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
defmodule Zexbox.AutoEscalation.AdfBuilder do
@moduledoc """
Builds Atlassian Document Format (ADF) maps for Jira issue descriptions and comments.

Mirrors `Opsbox::AutoEscalation::AdfBuilder`. Produces the same ADF structure so
headings, links, bullet lists, and the stack-trace expand render correctly in Jira.

Unlike the Ruby version, `stacktrace` must be passed explicitly (as `__STACKTRACE__`
from a `rescue` block) because Elixir exceptions do not carry their own backtrace.
"""

alias Zexbox.OpenTelemetry

@doc """
Builds an ADF description map for a new Jira issue.

Structure:
1. Telemetry links (Tempo | Kibana)
2. Divider (if `custom_description` present)
3. Custom description paragraphs (split on `\\n\\n`)
4. User context bullet list (if non-empty)
5. Additional context bullet list (if non-empty)
6. H3 "Error Details"
7. Exception summary + expandable stack trace
"""
@spec build_description(
Exception.t(),
map(),
map(),
Exception.stacktrace() | nil,
String.t() | nil
) :: map()
def build_description(
exception,
user_context,
additional_context,
stacktrace \\ nil,
custom_description \\ nil
) do
[]
|> build_body(exception, user_context, additional_context, stacktrace, custom_description)
|> doc()
end

@doc """
Builds an ADF comment map for an additional occurrence on an existing Jira issue.

Structure:
1. H2 "Additional Occurrence (action)"
2. Telemetry links
3. Divider + custom description (if present)
4. Context bullet lists (if non-empty)
5. H3 "Error Details"
6. Exception summary + expandable stack trace
"""
@spec build_comment(
Exception.t(),
String.t(),
map(),
map(),
Exception.stacktrace() | nil,
String.t() | nil
) :: map()
def build_comment(
exception,
action,
user_context,
additional_context,
stacktrace \\ nil,
custom_description \\ nil
) do
[heading(2, "Additional Occurrence (#{action})")]
|> build_body(exception, user_context, additional_context, stacktrace, custom_description)
|> doc()
end

# --- Private ---

defp build_body(
acc,
exception,
user_context,
additional_context,
stacktrace,
custom_description
) do
acc
|> append_telemetry()
|> append_description(custom_description)
|> append_context(user_context, additional_context)
|> append_error_details(exception, stacktrace)
end

defp append_telemetry(acc) do
trace_url = OpenTelemetry.generate_trace_url()
kibana_url = OpenTelemetry.kibana_log_url()

inline =
link_or_plain("Tempo Trace View", trace_url) ++
[text(" | ")] ++
link_or_plain("Kibana Logs", kibana_url)

acc ++ [%{type: "paragraph", content: inline}]
end

defp append_description(acc, nil), do: acc
defp append_description(acc, ""), do: acc

defp append_description(acc, desc) do
case String.trim(desc) do
"" -> acc
trimmed -> acc ++ [divider() | custom_description_blocks(trimmed)]
end
end

defp append_context(acc, user_context, additional_context) do
acc
|> append_single_context("User Context", user_context)
|> append_single_context("Additional Context", additional_context)
end

defp append_single_context(acc, _label, context) when not is_map(context), do: acc
defp append_single_context(acc, _label, context) when map_size(context) == 0, do: acc

defp append_single_context(acc, label, context) do
acc ++ [%{type: "paragraph", content: [bold(label)]}, key_value_bullet_list(context)]
end

defp append_error_details(acc, exception, stacktrace) do
error_class = inspect(exception.__struct__)
message = Exception.message(exception)
summary = "#{error_class}: #{message}"

backtrace =
case stacktrace do
nil -> "No stack trace available"
[] -> "No stack trace available"
st -> Exception.format_stacktrace(st)
end

acc ++
[
heading(3, "Error Details"),
%{type: "paragraph", content: [text(summary)]},
expand("Stack trace", [code_block(backtrace)])
]
end

defp custom_description_blocks(desc) do
desc
|> String.split(~r/\n\n+/)
|> Enum.map(fn paragraph ->
%{type: "paragraph", content: [text(String.trim(paragraph))]}
end)
end

defp key_value_bullet_list(hash) do
items =
Enum.map(hash, fn {key, value} ->
%{
type: "listItem",
content: [
%{
type: "paragraph",
content: [bold(key), text(": "), text(to_string(value))]
}
]
}
end)

%{type: "bulletList", content: items}
end

defp doc(content), do: %{version: 1, type: "doc", content: content}
defp text(str), do: %{type: "text", text: str}
defp bold(str), do: %{type: "text", text: to_string(str), marks: [%{type: "strong"}]}
defp divider, do: %{type: "rule"}

defp heading(level, text_content),
do: %{type: "heading", attrs: %{level: level}, content: [text(text_content)]}

defp code_block(content),
do: %{type: "codeBlock", content: [%{type: "text", text: to_string(content)}]}

defp expand(title, content_blocks),
do: %{type: "expand", attrs: %{title: title}, content: content_blocks}

defp link_or_plain(label, url) when is_binary(url) and url != "" do
[%{type: "text", text: label, marks: [%{type: "link", attrs: %{href: url}}]}]
end

defp link_or_plain(label, _url) do
[%{type: "text", text: "#{label} (Missing)"}]
end
end
Loading