Skip to content

Commit bfc9936

Browse files
daksh-rDakshclaude
authored
feat: add queryTeamUsageStats API for multi-tenant metrics (#236)
* add team usage stats api support * fix: exclude null fields from team usage stats request JSON Add @JsonInclude(NON_NULL) to prevent Jackson from serializing null values in the request body, which was causing 404 errors from the API. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * test: add strict integration tests for Team Usage Stats API Add TeamUsageStatsIntegrationTest with: - Strict data correctness tests for sdk-test-team-1/2/3 - Verifies exact values for all 16 metrics - Uses dedicated credentials (STREAM_USAGE_STATS_KEY/SECRET) - Queries fixed date range (2026-02-17 to 2026-02-18) - Catches API/SDK regressions by asserting exact values Also refactor TeamUsageStatsTest to require at least one team in response structure validation. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * test: add exact data verification for all query types Expand strict data correctness tests to cover ALL query methods: - Date range query (startDate/endDate) - Month query (month parameter) - No parameters query (default) - Pagination query (limit + next cursor) Each query type verifies exact values for all 16 metrics on sdk-test-team-1, sdk-test-team-2, sdk-test-team-3. Total: 34 integration tests for Team Usage Stats API. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * style: fix spotless formatting Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Use STREAM_MULTI_TENANT_KEY/SECRET for team usage stats tests - Rename env vars from STREAM_USAGE_STATS_* to STREAM_MULTI_TENANT_* - Remove fallback to STREAM_KEY/STREAM_SECRET - Fail explicitly with clear error message when credentials are missing Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix CI failures for team usage stats tests - Add STREAM_MULTI_TENANT_KEY/SECRET env vars to CI workflow - Fix TeamUsageStatsTest to not require data when account has no multi-tenant data (strict data verification is in TeamUsageStatsIntegrationTest) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: restore original client after TeamUsageStatsIntegrationTest The test was setting the global DefaultClient singleton to use multi-tenant credentials but never restoring it. This caused subsequent tests (like UserTest.canCreateGuestUser) to fail because guest access is disabled for multi-tenant apps. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: simplify TeamUsageStatsTest for non-multi-tenant app The regular test app doesn't have multi-tenant enabled, so teams will always be empty. Removed dead code inside if-blocks that would never execute and added explicit assertions that teams is empty. Full data verification is done in TeamUsageStatsIntegrationTest with dedicated multi-tenant credentials. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * resolve comments: remove hardcoded metric count, simplify tests - Remove "16 metrics" from javadoc (implementation detail) - Simplify TeamUsageStatsTest for non-multi-tenant app - Restore original client after TeamUsageStatsIntegrationTest Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Daksh <daksh.rinwan@getstream.io> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 39b3b42 commit bfc9936

File tree

5 files changed

+880
-0
lines changed

5 files changed

+880
-0
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ jobs:
2020
env:
2121
STREAM_KEY: ${{ secrets.STREAM_KEY }}
2222
STREAM_SECRET: ${{ secrets.STREAM_SECRET }}
23+
STREAM_MULTI_TENANT_KEY: ${{ secrets.STREAM_MULTI_TENANT_KEY }}
24+
STREAM_MULTI_TENANT_SECRET: ${{ secrets.STREAM_MULTI_TENANT_SECRET }}
2325
run: |
2426
./gradlew spotlessCheck --no-daemon
2527
./gradlew javadoc --no-daemon
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
package io.getstream.chat.java.models;
2+
3+
import com.fasterxml.jackson.annotation.JsonInclude;
4+
import com.fasterxml.jackson.annotation.JsonProperty;
5+
import io.getstream.chat.java.models.TeamUsageStats.QueryTeamUsageStatsRequestData.QueryTeamUsageStatsRequest;
6+
import io.getstream.chat.java.models.framework.StreamRequest;
7+
import io.getstream.chat.java.models.framework.StreamResponseObject;
8+
import io.getstream.chat.java.services.StatsService;
9+
import io.getstream.chat.java.services.framework.Client;
10+
import java.util.List;
11+
import lombok.Builder;
12+
import lombok.Data;
13+
import lombok.EqualsAndHashCode;
14+
import lombok.Getter;
15+
import lombok.NoArgsConstructor;
16+
import org.jetbrains.annotations.NotNull;
17+
import org.jetbrains.annotations.Nullable;
18+
import retrofit2.Call;
19+
20+
/** Team-level usage statistics for multi-tenant apps. */
21+
@Data
22+
@NoArgsConstructor
23+
public class TeamUsageStats {
24+
25+
/** Team identifier (empty string for users not assigned to any team). */
26+
@NotNull
27+
@JsonProperty("team")
28+
private String team;
29+
30+
// Daily activity metrics (total = SUM of daily values)
31+
32+
/** Daily active users. */
33+
@NotNull
34+
@JsonProperty("users_daily")
35+
private MetricStats usersDaily;
36+
37+
/** Daily messages sent. */
38+
@NotNull
39+
@JsonProperty("messages_daily")
40+
private MetricStats messagesDaily;
41+
42+
/** Daily translations. */
43+
@NotNull
44+
@JsonProperty("translations_daily")
45+
private MetricStats translationsDaily;
46+
47+
/** Daily image moderations. */
48+
@NotNull
49+
@JsonProperty("image_moderations_daily")
50+
private MetricStats imageModerationDaily;
51+
52+
// Peak metrics (total = MAX of daily values)
53+
54+
/** Peak concurrent users. */
55+
@NotNull
56+
@JsonProperty("concurrent_users")
57+
private MetricStats concurrentUsers;
58+
59+
/** Peak concurrent connections. */
60+
@NotNull
61+
@JsonProperty("concurrent_connections")
62+
private MetricStats concurrentConnections;
63+
64+
// Rolling/cumulative metrics (total = LATEST daily value)
65+
66+
/** Total users. */
67+
@NotNull
68+
@JsonProperty("users_total")
69+
private MetricStats usersTotal;
70+
71+
/** Users active in last 24 hours. */
72+
@NotNull
73+
@JsonProperty("users_last_24_hours")
74+
private MetricStats usersLast24Hours;
75+
76+
/** MAU - users active in last 30 days. */
77+
@NotNull
78+
@JsonProperty("users_last_30_days")
79+
private MetricStats usersLast30Days;
80+
81+
/** Users active this month. */
82+
@NotNull
83+
@JsonProperty("users_month_to_date")
84+
private MetricStats usersMonthToDate;
85+
86+
/** Engaged MAU. */
87+
@NotNull
88+
@JsonProperty("users_engaged_last_30_days")
89+
private MetricStats usersEngagedLast30Days;
90+
91+
/** Engaged users this month. */
92+
@NotNull
93+
@JsonProperty("users_engaged_month_to_date")
94+
private MetricStats usersEngagedMonthToDate;
95+
96+
/** Total messages. */
97+
@NotNull
98+
@JsonProperty("messages_total")
99+
private MetricStats messagesTotal;
100+
101+
/** Messages in last 24 hours. */
102+
@NotNull
103+
@JsonProperty("messages_last_24_hours")
104+
private MetricStats messagesLast24Hours;
105+
106+
/** Messages in last 30 days. */
107+
@NotNull
108+
@JsonProperty("messages_last_30_days")
109+
private MetricStats messagesLast30Days;
110+
111+
/** Messages this month. */
112+
@NotNull
113+
@JsonProperty("messages_month_to_date")
114+
private MetricStats messagesMonthToDate;
115+
116+
/** Statistics for a single metric with optional daily breakdown. */
117+
@Data
118+
@NoArgsConstructor
119+
public static class MetricStats {
120+
/** Per-day values (only present in daily mode). */
121+
@Nullable
122+
@JsonProperty("daily")
123+
private List<DailyValue> daily;
124+
125+
/** Aggregated total value. */
126+
@NotNull
127+
@JsonProperty("total")
128+
private Long total;
129+
}
130+
131+
/** Represents a metric value for a specific date. */
132+
@Data
133+
@NoArgsConstructor
134+
public static class DailyValue {
135+
/** Date in YYYY-MM-DD format. */
136+
@NotNull
137+
@JsonProperty("date")
138+
private String date;
139+
140+
/** Metric value for this date. */
141+
@NotNull
142+
@JsonProperty("value")
143+
private Long value;
144+
}
145+
146+
@Builder(
147+
builderClassName = "QueryTeamUsageStatsRequest",
148+
builderMethodName = "",
149+
buildMethodName = "internalBuild")
150+
@Getter
151+
@EqualsAndHashCode
152+
@JsonInclude(JsonInclude.Include.NON_NULL)
153+
public static class QueryTeamUsageStatsRequestData {
154+
/**
155+
* Month in YYYY-MM format (e.g., '2026-01'). Mutually exclusive with start_date/end_date.
156+
* Returns aggregated monthly values.
157+
*/
158+
@Nullable
159+
@JsonProperty("month")
160+
private String month;
161+
162+
/**
163+
* Start date in YYYY-MM-DD format. Used with end_date for custom date range. Returns daily
164+
* breakdown.
165+
*/
166+
@Nullable
167+
@JsonProperty("start_date")
168+
private String startDate;
169+
170+
/**
171+
* End date in YYYY-MM-DD format. Used with start_date for custom date range. Returns daily
172+
* breakdown.
173+
*/
174+
@Nullable
175+
@JsonProperty("end_date")
176+
private String endDate;
177+
178+
/** Maximum number of teams to return per page (default: 30, max: 30). */
179+
@Nullable
180+
@JsonProperty("limit")
181+
private Integer limit;
182+
183+
/** Cursor for pagination to fetch next page of teams. */
184+
@Nullable
185+
@JsonProperty("next")
186+
private String next;
187+
188+
public static class QueryTeamUsageStatsRequest
189+
extends StreamRequest<QueryTeamUsageStatsResponse> {
190+
@Override
191+
protected Call<QueryTeamUsageStatsResponse> generateCall(Client client) {
192+
return client.create(StatsService.class).queryTeamUsageStats(this.internalBuild());
193+
}
194+
}
195+
}
196+
197+
@Data
198+
@NoArgsConstructor
199+
@EqualsAndHashCode(callSuper = true)
200+
public static class QueryTeamUsageStatsResponse extends StreamResponseObject {
201+
/** Array of team usage statistics. */
202+
@NotNull
203+
@JsonProperty("teams")
204+
private List<TeamUsageStats> teams;
205+
206+
/** Cursor for pagination to fetch next page. */
207+
@Nullable
208+
@JsonProperty("next")
209+
private String next;
210+
}
211+
212+
/**
213+
* Queries team-level usage statistics from the warehouse database.
214+
*
215+
* <p>Returns usage metrics grouped by team with cursor-based pagination.
216+
*
217+
* <p>Date Range Options (mutually exclusive):
218+
*
219+
* <ul>
220+
* <li>Use 'month' parameter (YYYY-MM format) for monthly aggregated values
221+
* <li>Use 'startDate'/'endDate' parameters (YYYY-MM-DD format) for daily breakdown
222+
* <li>If neither provided, defaults to current month (monthly mode)
223+
* </ul>
224+
*
225+
* <p>This endpoint is server-side only.
226+
*
227+
* @return the created request
228+
*/
229+
@NotNull
230+
public static QueryTeamUsageStatsRequest queryTeamUsageStats() {
231+
return new QueryTeamUsageStatsRequest();
232+
}
233+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package io.getstream.chat.java.services;
2+
3+
import io.getstream.chat.java.models.TeamUsageStats.QueryTeamUsageStatsRequestData;
4+
import io.getstream.chat.java.models.TeamUsageStats.QueryTeamUsageStatsResponse;
5+
import org.jetbrains.annotations.NotNull;
6+
import retrofit2.Call;
7+
import retrofit2.http.Body;
8+
import retrofit2.http.POST;
9+
10+
public interface StatsService {
11+
@POST("stats/team_usage")
12+
Call<QueryTeamUsageStatsResponse> queryTeamUsageStats(
13+
@NotNull @Body QueryTeamUsageStatsRequestData queryTeamUsageStatsRequestData);
14+
}

0 commit comments

Comments
 (0)