Skip to content

feat(weekly-report): Add Total Spans header to weekly report#119787

Draft
amy-chen23 wants to merge 6 commits into
masterfrom
amychen/id-1680-spans-total-header
Draft

feat(weekly-report): Add Total Spans header to weekly report#119787
amy-chen23 wants to merge 6 commits into
masterfrom
amychen/id-1680-spans-total-header

Conversation

@amy-chen23

Copy link
Copy Markdown
Contributor

Summary

  • Query top 5 transaction spans by sum(span.duration) with count() to get total span count
  • Add "Total Spans" header section to weekly report email with abbreviated count and "View All Spans" link
  • Filter span count per user's project access
  • Gated behind organizations:weekly-report-spans-chart feature flag

Details

Data layer (utils.py):

  • organization_top_spans() now queries count() alongside sum(span.duration) and p95(span.duration)
  • total_spans_count field on OrganizationReportContext sums counts across all queried spans
  • Escapes backslashes and double-quotes in span name query filters

Template (body.html):

  • "Total Spans" section after project breakdown, before issue type breakdown
  • Uses small_count filter for abbreviated display (e.g. "4.3m")
  • "View All Spans" links to /explore/spans/

Debug preview:

  • Synthetic span data with randomized counts for debug view at /debug/mail/reports/body/

Test plan

  • test_organization_top_spans_populates_context
  • test_organization_top_spans_skips_without_transactions
  • test_organization_top_spans_timeseries_populates_context
  • test_organization_top_spans_timeseries_skips_without_top_spans
  • test_enhanced_privacy_skips_top_spans
  • test_top_spans_feature_flag_gates_query
  • test_top_spans_projects_filters_by_user_access
  • Visual check via /debug/mail/reports/body/

amy-chen23 and others added 5 commits July 15, 2026 13:33
Span names containing backslashes could produce malformed search
filters because only double-quote escaping was applied. Escape
backslashes first, matching the pattern used by
_escape_search_query_value in the EAP search module.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
Add a Total Spans section showing the aggregate span count with a
View All Spans link. The count is filtered by user project access
and gated behind the weekly-report-spans-chart feature flag.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
@linear-code

linear-code Bot commented Jul 15, 2026

Copy link
Copy Markdown

ID-1680

@github-actions github-actions Bot added the Scope: Backend Automatically applied to PRs that change backend components label Jul 15, 2026
Comment on lines +741 to +787
for row in result.get("data", []):
span_name = row.get("span.name", "")
if not span_name:
continue
count = row.get("count()", 0)
ctx.top_spans.append(
{
"name": span_name,
"p95": row.get("p95(span.duration)", 0),
"sum": row.get("sum(span.duration)", 0),
"count": count,
}
)
top_span_names.append(span_name)
total_count += count
ctx.total_spans_count = total_count

if not top_span_names:
return

with start_span(
op="weekly_reports.top_spans_projects", name="weekly_reports.top_spans_projects"
):
span_name_filter = " OR ".join(
'span.name:"{}"'.format(name.replace("\\", "\\\\").replace('"', '\\"'))
for name in top_span_names[:TOP_SPANS_QUERY_LIMIT]
)
project_result = Spans.run_table_query(
params=snuba_params,
query_string=f"is_transaction:1 ({span_name_filter})",
selected_columns=["span.name", "project.id", "count()"],
orderby=["-count()"],
offset=0,
limit=TOP_SPANS_QUERY_LIMIT * 10,
referrer=referrer,
config=config,
sampling_mode=None,
)

for row in project_result.get("data", []):
span_name = row.get("span.name", "")
project_id = row.get("project.id")
if span_name and project_id:
ctx.top_spans_projects.setdefault(span_name, set()).add(int(project_id))


def organization_top_spans_timeseries(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Weekly report Total Spans includes org-wide count for spans the user only partially owns

Total Spans is computed from org-wide count() per top span and only gates on whether the span name appears in any of the recipient's accessible projects, so span volume from projects the user cannot access is included whenever a span name is shared. Store and sum per-project counts restricted to the user's project set instead of the org-level aggregate.

Evidence
  • organization_top_spans() in utils.py queries all org projects (ctx.projects_context_map) and stores each row's org-wide count() on ctx.top_spans[i]["count"].
  • The second query only records which project IDs a span name appears in (ctx.top_spans_projects[name] set); the per-project counts are discarded.
  • render_template_context() in weekly_reports.py computes sum(span["count"] ... if ctx.top_spans_projects.get(span["name"]) & user_project_ids), so any intersection with a single owned project adds the full org-wide count.
  • Impact is limited: the disclosed data is an aggregate span-count number for span names the user can already see, gated by the organizations:weekly-report-spans-chart flag and enhanced_privacy.
Also found at 1 additional location
  • src/sentry/tasks/summaries/weekly_reports.py:858-863

Identified by Warden security-review · DLM-K72

Add an HTML table below the Total Spans header showing span
description, project(s), p95 duration, and total duration for the
top 5 spans. Includes a format_duration_ms template filter for
human-readable duration formatting.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Scope: Backend Automatically applied to PRs that change backend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant