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);