Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
8984411
update weekly report dev environment
amy-chen23 Jun 2, 2026
e120c09
feature flag for cache api endpoint
amy-chen23 Jun 2, 2026
a29ab0d
cache logic for weekly report. cache is populated when the weekly rep…
amy-chen23 Jun 2, 2026
8f90afe
:hammer_and_wrench: Sync API Urls to TypeScript
getsantry[bot] Jun 2, 2026
103b522
gate cache layer behind feature flag
amy-chen23 Jun 2, 2026
419adb7
moving imports up
amy-chen23 Jun 2, 2026
1bab33b
Merge branch 'master' into amyc/cache-weekly-report-metrics
amy-chen23 Jun 2, 2026
b6f5ee3
removing frontend changes
amy-chen23 Jun 3, 2026
c076d81
removing frontend change (for real this time)
amy-chen23 Jun 3, 2026
0e4314c
:hammer_and_wrench: Sync API Urls to TypeScript
getsantry[bot] Jun 3, 2026
8edfe8b
fixing cache miss issues with SATURDAY send time
amy-chen23 Jun 3, 2026
20fd594
imported correct redis cluster. also added -> None to all test method…
amy-chen23 Jun 3, 2026
fa2b08c
removed API endpoint logic
amy-chen23 Jun 8, 2026
a49272d
removing file diff
amy-chen23 Jun 8, 2026
cf72ce3
adding cache miss metrics
amy-chen23 Jun 8, 2026
2d0b585
adding math.floor for saturday (so less cache misses)
amy-chen23 Jun 8, 2026
59edfc4
removed timestamp from cache key
amy-chen23 Jun 9, 2026
94689e7
no caching during dry run
amy-chen23 Jun 9, 2026
bde2262
added feature flag. moved metric extraction to weekly_reports.py so t…
amy-chen23 Jun 9, 2026
732aa53
removing feature flag and wrapping caching in a try-catch
amy-chen23 Jun 9, 2026
8c2ece0
remove extra conversion of cache ttl
amy-chen23 Jun 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
59 changes: 59 additions & 0 deletions src/sentry/tasks/summaries/weekly_report_cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from __future__ import annotations

from typing import Any

from django.conf import settings
from sentry_redis_tools.clients import RedisCluster, StrictRedis

from sentry.utils import json, metrics, redis

CACHE_TTL_SEC = 10 * 24 * 60 * 60 # 10 days
KEY_PREFIX = "wr:proj_metrics"


def _make_cache_key(org_id: int, project_id: int) -> str:
return f"{KEY_PREFIX}:{org_id}:{project_id}"


def _get_redis_client() -> RedisCluster[str] | StrictRedis[str]:
return redis.redis_clusters.get(settings.SENTRY_WEEKLY_REPORTS_REDIS_CLUSTER)


def cache_project_metrics(
org_id: int,
project_metrics: dict[int, dict[str, int]],
) -> None:
client = _get_redis_client()
pipeline = client.pipeline()

for project_id, values in project_metrics.items():
key = _make_cache_key(org_id, project_id)
pipeline.set(key, json.dumps(values), ex=CACHE_TTL_SEC)

pipeline.execute()


def read_project_metrics(
org_id: int,
project_ids: list[int],
) -> dict[int, dict[str, Any]]:
if not project_ids:
return {}

client = _get_redis_client()
pipeline = client.pipeline()

for project_id in project_ids:
pipeline.get(_make_cache_key(org_id, project_id))

results = pipeline.execute()

result_map: dict[int, dict[str, Any]] = {}
for i, project_id in enumerate(project_ids):
raw = results[i]
if raw is None:
metrics.incr("weekly_report.cache.miss")
else:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

should we record cache hits as well? another way you could do this would be to have a single metric called weekly_report.cache_read and then a tag on the metric is set to either "miss" or "hit" based on the result, and then DD allows you to break down those timeseries

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this can be part of your next PR btw, doesn't have to block this one

result_map[project_id] = json.loads(raw)

return result_map
15 changes: 15 additions & 0 deletions src/sentry/tasks/summaries/weekly_reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
OrganizationReportContextFactory,
)
from sentry.tasks.summaries.utils import ONE_DAY, OrganizationReportContext
from sentry.tasks.summaries.weekly_report_cache import cache_project_metrics
from sentry.taskworker.namespaces import reports_tasks
from sentry.types.group import GroupSubStatus
from sentry.users.services.user_option import user_option_service
Expand Down Expand Up @@ -217,6 +218,20 @@ def prepare_organization_report(
lifecycle.record_halt(WeeklyReportHaltReason.EMPTY_REPORT)
return

if not dry_run:
try:
project_metrics: dict[int, dict[str, int]] = {}
for project_id, project_ctx in ctx.projects_context_map.items():
if not project_ctx.check_if_project_is_empty():
project_metrics[project_id] = {
"e": project_ctx.accepted_error_count,
"t": project_ctx.accepted_transaction_count,
}
if project_metrics:
cache_project_metrics(organization_id, project_metrics)
except Exception:
sentry_sdk.capture_exception()

# Finally, deliver the reports
batch = OrganizationReportBatch(ctx, batch_id, dry_run, target_user, email_override)
with sentry_sdk.start_span(op="weekly_reports.deliver_reports"):
Expand Down
53 changes: 53 additions & 0 deletions tests/sentry/tasks/test_weekly_report_cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from sentry.tasks.summaries.weekly_report_cache import (
_make_cache_key,
cache_project_metrics,
read_project_metrics,
)
from sentry.testutils.cases import TestCase


class WeeklyReportCacheTest(TestCase):
def test_make_cache_key(self) -> None:
key = _make_cache_key(org_id=1, project_id=2)
assert key == "wr:proj_metrics:1:2"

def test_write_and_read(self) -> None:
org_id = self.organization.id
project = self.create_project(organization=self.organization)

cache_project_metrics(org_id, {project.id: {"e": 500, "t": 3000}})

result = read_project_metrics(org_id=org_id, project_ids=[project.id])

assert result[project.id] == {"e": 500, "t": 3000}

def test_read_empty_cache(self) -> None:
result = read_project_metrics(org_id=self.organization.id, project_ids=[999])

assert result == {}

def test_read_empty_project_ids(self) -> None:
result = read_project_metrics(org_id=self.organization.id, project_ids=[])

assert result == {}

def test_write_empty_metrics_is_noop(self) -> None:
cache_project_metrics(self.organization.id, {})

def test_multiple_projects(self) -> None:
org_id = self.organization.id
p1 = self.create_project(organization=self.organization)
p2 = self.create_project(organization=self.organization)

cache_project_metrics(
org_id,
{
p1.id: {"e": 100, "t": 200},
p2.id: {"e": 300, "t": 400},
},
)

result = read_project_metrics(org_id=org_id, project_ids=[p1.id, p2.id])

assert result[p1.id] == {"e": 100, "t": 200}
assert result[p2.id] == {"e": 300, "t": 400}
Loading