diff --git a/src/Clients/IStatsClient.cs b/src/Clients/IStatsClient.cs new file mode 100644 index 00000000..b5fc42bf --- /dev/null +++ b/src/Clients/IStatsClient.cs @@ -0,0 +1,24 @@ +using System.Threading.Tasks; +using StreamChat.Models; + +namespace StreamChat.Clients +{ + /// + /// A client that can be used to query team-level usage statistics. + /// + public interface IStatsClient + { + /// + /// Queries team-level usage statistics from the warehouse database. + /// Returns all 16 metrics grouped by team with cursor-based pagination. + /// This endpoint is server-side only. + /// Date Range Options (mutually exclusive): + /// + /// Use 'month' parameter (YYYY-MM format) for monthly aggregated values + /// Use 'start_date'/'end_date' parameters (YYYY-MM-DD format) for daily breakdown + /// If neither provided, defaults to current month (monthly mode) + /// + /// + Task QueryTeamUsageStatsAsync(QueryTeamUsageStatsOptions options = null); + } +} diff --git a/src/Clients/IStreamClientFactory.cs b/src/Clients/IStreamClientFactory.cs index eace2a96..20d61e9a 100644 --- a/src/Clients/IStreamClientFactory.cs +++ b/src/Clients/IStreamClientFactory.cs @@ -101,5 +101,10 @@ public interface IStreamClientFactory /// /// https://getstream.io/chat/docs/dotnet-csharp/threads/?language=csharp#filtering-and-sorting-threads IThreadClient GetThreadClient(); + + /// + /// Returns an instance. The returned client can be used as a singleton in your application. + /// + IStatsClient GetStatsClient(); } } diff --git a/src/Clients/StatsClient.cs b/src/Clients/StatsClient.cs new file mode 100644 index 00000000..3d39145b --- /dev/null +++ b/src/Clients/StatsClient.cs @@ -0,0 +1,20 @@ +using System.Net; +using System.Threading.Tasks; +using StreamChat.Models; +using StreamChat.Rest; + +namespace StreamChat.Clients +{ + public class StatsClient : ClientBase, IStatsClient + { + internal StatsClient(IRestClient client) : base(client) + { + } + + public async Task QueryTeamUsageStatsAsync(QueryTeamUsageStatsOptions options = null) + => await ExecuteRequestAsync("stats/team_usage", + HttpMethod.POST, + HttpStatusCode.Created, + options ?? new QueryTeamUsageStatsOptions()); + } +} diff --git a/src/Clients/StreamClientFactory.cs b/src/Clients/StreamClientFactory.cs index d2561242..66d51629 100644 --- a/src/Clients/StreamClientFactory.cs +++ b/src/Clients/StreamClientFactory.cs @@ -27,6 +27,7 @@ public class StreamClientFactory : IStreamClientFactory private readonly ITaskClient _taskClient; private readonly IModerationClient _moderationClient; private readonly IThreadClient _threadClient; + private readonly IStatsClient _statsClient; /// /// Initializes a new instance of the class. @@ -94,6 +95,7 @@ public StreamClientFactory(string apiKey, string apiSecret, Action _appClient; @@ -112,5 +114,6 @@ public StreamClientFactory(string apiKey, string apiSecret, Action _userClient; public IModerationClient GetModerationClient() => _moderationClient; public IThreadClient GetThreadClient() => _threadClient; + public IStatsClient GetStatsClient() => _statsClient; } } diff --git a/src/Models/Import.cs b/src/Models/Import.cs index 46f8c69b..332f9d40 100644 --- a/src/Models/Import.cs +++ b/src/Models/Import.cs @@ -25,6 +25,9 @@ public enum ImportState { None, + [EnumMember(Value = "queued")] + Queued, + [EnumMember(Value = "uploaded")] Uploaded, diff --git a/src/Models/TeamUsageStats.cs b/src/Models/TeamUsageStats.cs new file mode 100644 index 00000000..5756b5dc --- /dev/null +++ b/src/Models/TeamUsageStats.cs @@ -0,0 +1,160 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace StreamChat.Models +{ + /// + /// Represents a metric value for a specific date. + /// + public class DailyValue + { + /// Date in YYYY-MM-DD format. + [JsonProperty("date")] + public string Date { get; set; } + + /// Metric value for this date. + [JsonProperty("value")] + public long Value { get; set; } + } + + /// + /// Statistics for a single metric with optional daily breakdown. + /// + public class MetricStats + { + /// Per-day values (only present in daily mode). + [JsonProperty("daily")] + public List Daily { get; set; } + + /// Aggregated total value. + [JsonProperty("total")] + public long Total { get; set; } + } + + /// + /// Team-level usage statistics for multi-tenant apps. + /// + public class TeamUsageStats + { + /// Team identifier (empty string for users not assigned to any team). + [JsonProperty("team")] + public string Team { get; set; } + + // Daily activity metrics (total = SUM of daily values) + + /// Daily active users. + [JsonProperty("users_daily")] + public MetricStats UsersDaily { get; set; } + + /// Daily messages sent. + [JsonProperty("messages_daily")] + public MetricStats MessagesDaily { get; set; } + + /// Daily translations. + [JsonProperty("translations_daily")] + public MetricStats TranslationsDaily { get; set; } + + /// Daily image moderations. + [JsonProperty("image_moderations_daily")] + public MetricStats ImageModerationsDaily { get; set; } + + // Peak metrics (total = MAX of daily values) + + /// Peak concurrent users. + [JsonProperty("concurrent_users")] + public MetricStats ConcurrentUsers { get; set; } + + /// Peak concurrent connections. + [JsonProperty("concurrent_connections")] + public MetricStats ConcurrentConnections { get; set; } + + // Rolling/cumulative metrics (total = LATEST daily value) + + /// Total users. + [JsonProperty("users_total")] + public MetricStats UsersTotal { get; set; } + + /// Users active in last 24 hours. + [JsonProperty("users_last_24_hours")] + public MetricStats UsersLast24Hours { get; set; } + + /// MAU - users active in last 30 days. + [JsonProperty("users_last_30_days")] + public MetricStats UsersLast30Days { get; set; } + + /// Users active this month. + [JsonProperty("users_month_to_date")] + public MetricStats UsersMonthToDate { get; set; } + + /// Engaged MAU. + [JsonProperty("users_engaged_last_30_days")] + public MetricStats UsersEngagedLast30Days { get; set; } + + /// Engaged users this month. + [JsonProperty("users_engaged_month_to_date")] + public MetricStats UsersEngagedMonthToDate { get; set; } + + /// Total messages. + [JsonProperty("messages_total")] + public MetricStats MessagesTotal { get; set; } + + /// Messages in last 24 hours. + [JsonProperty("messages_last_24_hours")] + public MetricStats MessagesLast24Hours { get; set; } + + /// Messages in last 30 days. + [JsonProperty("messages_last_30_days")] + public MetricStats MessagesLast30Days { get; set; } + + /// Messages this month. + [JsonProperty("messages_month_to_date")] + public MetricStats MessagesMonthToDate { get; set; } + } + + /// + /// Options for querying team usage stats. + /// + public class QueryTeamUsageStatsOptions + { + /// + /// Month in YYYY-MM format (e.g., '2026-01'). Mutually exclusive with start_date/end_date. + /// Returns aggregated monthly values. + /// + [JsonProperty("month")] + public string Month { get; set; } + + /// + /// Start date in YYYY-MM-DD format. Used with end_date for custom date range. Returns daily breakdown. + /// + [JsonProperty("start_date")] + public string StartDate { get; set; } + + /// + /// End date in YYYY-MM-DD format. Used with start_date for custom date range. Returns daily breakdown. + /// + [JsonProperty("end_date")] + public string EndDate { get; set; } + + /// Maximum number of teams to return per page (default: 30, max: 30). + [JsonProperty("limit")] + public int? Limit { get; set; } + + /// Cursor for pagination to fetch next page of teams. + [JsonProperty("next")] + public string Next { get; set; } + } + + /// + /// Response from querying team usage stats. + /// + public class QueryTeamUsageStatsResponse : ApiResponse + { + /// Array of team usage statistics. + [JsonProperty("teams")] + public List Teams { get; set; } + + /// Cursor for pagination to fetch next page. + [JsonProperty("next")] + public string Next { get; set; } + } +} diff --git a/tests/StatsClientTests.cs b/tests/StatsClientTests.cs new file mode 100644 index 00000000..54ef646f --- /dev/null +++ b/tests/StatsClientTests.cs @@ -0,0 +1,125 @@ +using System; +using System.Threading.Tasks; +using FluentAssertions; +using NUnit.Framework; +using StreamChat.Models; + +namespace StreamChatTests +{ + /// Tests for + /// + /// The tests follow arrange-act-assert pattern divided by empty lines. + /// Please make sure to follow the pattern to keep the consistency. + /// + [TestFixture] + public class StatsClientTests : TestBase + { + [Test] + public async Task TestQueryTeamUsageStatsDefault() + { + var response = await _statsClient.QueryTeamUsageStatsAsync(); + + response.Teams.Should().NotBeNull(); + } + + [Test] + public async Task TestQueryTeamUsageStatsWithMonth() + { + var currentMonth = DateTime.UtcNow.ToString("yyyy-MM"); + + var response = await _statsClient.QueryTeamUsageStatsAsync(new QueryTeamUsageStatsOptions + { + Month = currentMonth, + }); + + response.Teams.Should().NotBeNull(); + } + + [Test] + public async Task TestQueryTeamUsageStatsWithDateRange() + { + var endDate = DateTime.UtcNow; + var startDate = endDate.AddDays(-7); + + var response = await _statsClient.QueryTeamUsageStatsAsync(new QueryTeamUsageStatsOptions + { + StartDate = startDate.ToString("yyyy-MM-dd"), + EndDate = endDate.ToString("yyyy-MM-dd"), + }); + + response.Teams.Should().NotBeNull(); + } + + [Test] + public async Task TestQueryTeamUsageStatsWithPagination() + { + var response = await _statsClient.QueryTeamUsageStatsAsync(new QueryTeamUsageStatsOptions + { + Limit = 10, + }); + + response.Teams.Should().NotBeNull(); + + // If there's a next cursor, fetch the next page + if (!string.IsNullOrEmpty(response.Next)) + { + var nextResponse = await _statsClient.QueryTeamUsageStatsAsync(new QueryTeamUsageStatsOptions + { + Limit = 10, + Next = response.Next, + }); + + nextResponse.Teams.Should().NotBeNull(); + } + } + + [Test] + public async Task TestQueryTeamUsageStatsResponseStructure() + { + // Query last year to maximize chance of getting data + var endDate = DateTime.UtcNow; + var startDate = endDate.AddDays(-365); + + var response = await _statsClient.QueryTeamUsageStatsAsync(new QueryTeamUsageStatsOptions + { + StartDate = startDate.ToString("yyyy-MM-dd"), + EndDate = endDate.ToString("yyyy-MM-dd"), + }); + + response.Teams.Should().NotBeNull(); + + if (response.Teams.Count > 0) + { + var team = response.Teams[0]; + + // Verify team identifier + team.Team.Should().NotBeNull(); + + // Verify daily activity metrics + team.UsersDaily.Should().NotBeNull(); + team.MessagesDaily.Should().NotBeNull(); + team.TranslationsDaily.Should().NotBeNull(); + team.ImageModerationsDaily.Should().NotBeNull(); + + // Verify peak metrics + team.ConcurrentUsers.Should().NotBeNull(); + team.ConcurrentConnections.Should().NotBeNull(); + + // Verify rolling/cumulative metrics + team.UsersTotal.Should().NotBeNull(); + team.UsersLast24Hours.Should().NotBeNull(); + team.UsersLast30Days.Should().NotBeNull(); + team.UsersMonthToDate.Should().NotBeNull(); + team.UsersEngagedLast30Days.Should().NotBeNull(); + team.UsersEngagedMonthToDate.Should().NotBeNull(); + team.MessagesTotal.Should().NotBeNull(); + team.MessagesLast24Hours.Should().NotBeNull(); + team.MessagesLast30Days.Should().NotBeNull(); + team.MessagesMonthToDate.Should().NotBeNull(); + + // Verify metric structure + team.UsersDaily.Total.Should().BeGreaterOrEqualTo(0); + } + } + } +} diff --git a/tests/TestBase.cs b/tests/TestBase.cs index fb8f52fd..d4d79628 100644 --- a/tests/TestBase.cs +++ b/tests/TestBase.cs @@ -26,6 +26,7 @@ public abstract class TestBase protected static readonly ITaskClient _taskClient = TestClientFactory.GetTaskClient(); protected static readonly IModerationClient _moderationClient = TestClientFactory.GetModerationClient(); protected static readonly IThreadClient _threadClient = TestClientFactory.GetThreadClient(); + protected static readonly IStatsClient _statsClient = TestClientFactory.GetStatsClient(); private readonly List _testChannels = new List(); private readonly List _testChannelTypes = new List(); diff --git a/tests/TestClientFactory.cs b/tests/TestClientFactory.cs index d3b92932..9b74a266 100644 --- a/tests/TestClientFactory.cs +++ b/tests/TestClientFactory.cs @@ -26,5 +26,6 @@ public static class TestClientFactory public static ITaskClient GetTaskClient() => _clientFactory.GetTaskClient(); public static IModerationClient GetModerationClient() => _clientFactory.GetModerationClient(); public static IThreadClient GetThreadClient() => _clientFactory.GetThreadClient(); + public static IStatsClient GetStatsClient() => _clientFactory.GetStatsClient(); } }