From 23698876b377d26c296c299f0038803ac0d7e025 Mon Sep 17 00:00:00 2001 From: Roy Date: Mon, 14 Sep 2026 12:25:19 +0900 Subject: [PATCH] =?UTF-8?q?Sentry=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85?= =?UTF-8?q?=20=EC=8B=A0=ED=98=B8=EB=A5=BC=20=EA=B4=80=EB=A6=AC=EC=9E=90=20?= =?UTF-8?q?=EC=A7=80=ED=91=9C=EC=97=90=EC=84=9C=20=EC=A7=81=EC=A0=91=20?= =?UTF-8?q?=ED=8C=90=EB=8B=A8=ED=95=98=EA=B2=8C=20=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit trace metric의 숫자 값은 sum(value)로 집계하고 user.action.count에서 event=sign_up을 일별 시계열로 분리한다. Constraint: Picke-iOS Sentry metric contract and official trace metrics value alias Rejected: Mixpanel 회원가입 값을 Sentry 원천 값으로 표시 | 데이터 출처가 다름 Confidence: high Scope-risk: moderate Directive: Sentry counter metrics use sum(value), sign_up query remains quoted metric.name Tested: SentryClientTest; live Sentry timeseries HTTP 200; git diff --check Not-tested: current period has no Sentry sign_up sample, so non-zero production value --- .../domain/admin/analytics/SentryClient.java | 56 ++++++++++++++----- .../admin/analytics/SentryClientTest.java | 13 ++++- 2 files changed, 53 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/swyp/picke/domain/admin/analytics/SentryClient.java b/src/main/java/com/swyp/picke/domain/admin/analytics/SentryClient.java index e0a7cb9..cbd5ddc 100644 --- a/src/main/java/com/swyp/picke/domain/admin/analytics/SentryClient.java +++ b/src/main/java/com/swyp/picke/domain/admin/analytics/SentryClient.java @@ -101,9 +101,10 @@ private SentryIssueReport.ProjectIssues fetchProject(String project, LocalDate f List issues = parseIssues(issueResponse.body()); List days = parseDays(statsResponse.body(), from, to); List recentEvents = parseEvents(eventResponse.body()); - List datasets = DATASETS.stream() + List datasets = new ArrayList<>(DATASETS.stream() .map(dataset -> fetchDataset(project, dataset, from, to)) - .toList(); + .toList()); + datasets.add(fetchSignUps(project, from, to)); SentryIssueReport.MetricCatalog metricCatalog = fetchMetricCatalog(project, from, to); SentryIssueReport.SessionHealth sessionHealth = fetchSessionHealth(project, from, to); SentryIssueReport.ResourceCatalog releases = fetchReleases(project); @@ -169,35 +170,64 @@ private URI eventUri(String project, LocalDate from, LocalDate to) { private SentryIssueReport.DatasetSeries fetchDataset( String project, String dataset, LocalDate from, LocalDate to) { - AnalyticsHttpResponse response = transport.get(datasetUri(project, dataset, from, to), + String yAxis = dataset.equals("tracemetrics") ? "sum(value)" : "count()"; + return fetchDataset(project, dataset, dataset, yAxis, null, from, to); + } + + private SentryIssueReport.DatasetSeries fetchSignUps( + String project, LocalDate from, LocalDate to) { + return fetchDataset(project, "sign_up", "tracemetrics", "sum(value)", + "metric.name:\"user.action.count\" event:sign_up", from, to); + } + + private SentryIssueReport.DatasetSeries fetchDataset( + String project, + String responseName, + String sentryDataset, + String yAxis, + String query, + LocalDate from, + LocalDate to) { + AnalyticsHttpResponse response = transport.get( + datasetUri(project, sentryDataset, yAxis, query, from, to), Map.of("Authorization", "Bearer " + authToken)); if (!response.isSuccess() || !response.isJson()) { log.warn("[Sentry] 데이터셋 조회 실패: project={}, dataset={}, status={}", - project, dataset, response.statusCode()); - return new SentryIssueReport.DatasetSeries(dataset, AnalyticsStatus.UNAVAILABLE, null, List.of()); + project, responseName, response.statusCode()); + return new SentryIssueReport.DatasetSeries( + responseName, AnalyticsStatus.UNAVAILABLE, null, List.of()); } try { List days = parseDatasetDays(response.body(), from, to); long total = days.stream().mapToLong(SentryIssueReport.Day::events).sum(); - return new SentryIssueReport.DatasetSeries(dataset, AnalyticsStatus.CONNECTED, total, days); + return new SentryIssueReport.DatasetSeries(responseName, AnalyticsStatus.CONNECTED, total, days); } catch (Exception e) { log.warn("[Sentry] 데이터셋 파싱 실패: project={}, dataset={}, {}", - project, dataset, e.getClass().getSimpleName()); - return new SentryIssueReport.DatasetSeries(dataset, AnalyticsStatus.UNAVAILABLE, null, List.of()); + project, responseName, e.getClass().getSimpleName()); + return new SentryIssueReport.DatasetSeries( + responseName, AnalyticsStatus.UNAVAILABLE, null, List.of()); } } - private URI datasetUri(String project, String dataset, LocalDate from, LocalDate to) { - return UriComponentsBuilder.fromUriString(baseUrl) + private URI datasetUri( + String project, + String dataset, + String yAxis, + String query, + LocalDate from, + LocalDate to) { + UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(baseUrl) .path("/api/0/organizations/{organization}/events-timeseries/") .queryParam("project", project) .queryParam("dataset", dataset) .queryParam("start", from.atStartOfDay()) .queryParam("end", to.atTime(LocalTime.MAX).withNano(0)) .queryParam("interval", 86400) - .queryParam("yAxis", dataset.equals("tracemetrics") ? "count(metric)" : "count()") - .buildAndExpand(organization) - .toUri(); + .queryParam("yAxis", yAxis); + if (StringUtils.hasText(query)) { + builder.queryParam("query", query); + } + return builder.buildAndExpand(organization).toUri(); } private SentryIssueReport.MetricCatalog fetchMetricCatalog(String project, LocalDate from, LocalDate to) { diff --git a/src/test/java/com/swyp/picke/domain/admin/analytics/SentryClientTest.java b/src/test/java/com/swyp/picke/domain/admin/analytics/SentryClientTest.java index cb6f0a2..f63a03d 100644 --- a/src/test/java/com/swyp/picke/domain/admin/analytics/SentryClientTest.java +++ b/src/test/java/com/swyp/picke/domain/admin/analytics/SentryClientTest.java @@ -145,7 +145,7 @@ void fetchesEveryProjectWithAbsoluteRange() { var result = client("token", transport).fetchUnresolvedIssues(from, to); assertThat(transport.headers).containsEntry("Authorization", "Bearer token"); - assertThat(transport.uris).hasSize(22); + assertThat(transport.uris).hasSize(24); assertThat(transport.uris.getFirst().toString()) .contains("/api/0/projects/picke/picke-ios/issues/") .contains("query=is:unresolved") @@ -173,7 +173,13 @@ void fetchesEveryProjectWithAbsoluteRange() { .contains("/api/0/organizations/picke/events-timeseries/") && uri.toString().contains("project=picke-ios") && uri.toString().contains("dataset=tracemetrics") - && uri.toString().contains("yAxis=count(metric)")); + && uri.toString().contains("yAxis=sum(value)")); + assertThat(transport.uris).anyMatch(uri -> uri.toString() + .contains("/api/0/organizations/picke/events-timeseries/") + && uri.toString().contains("project=picke-ios") + && uri.toString().contains("dataset=tracemetrics") + && uri.toString().contains("yAxis=sum(value)") + && uri.toString().contains("event:sign_up")); assertThat(result.status()).isEqualTo(AnalyticsStatus.CONNECTED); assertThat(result.fetchedAt()).isNotNull(); assertThat(result.projects()).extracting(SentryIssueReport.ProjectIssues::project, @@ -211,7 +217,8 @@ void fetchesEveryProjectWithAbsoluteRange() { tuple("logs", AnalyticsStatus.CONNECTED, 6L), tuple("spans", AnalyticsStatus.CONNECTED, 6L), tuple("profile_functions", AnalyticsStatus.CONNECTED, 6L), - tuple("tracemetrics", AnalyticsStatus.CONNECTED, 6L)); + tuple("tracemetrics", AnalyticsStatus.CONNECTED, 6L), + tuple("sign_up", AnalyticsStatus.CONNECTED, 6L)); assertThat(result.projects().getFirst().metricCatalog().entries().getFirst()) .containsEntry("name", "app.launch.count") .containsEntry("count", 8);