NOTE: Apart from
(and even then it's questionable, I'm Scottish). These are machine translated in languages I don't read. If they're terrible please contact me.
You can see how this translation was done in this article.
Wednesday, 14 August 2024
//Less than a minute
In this post, I will show you how to create a C# client for the Umami reporting API. This is a simple example that demonstrates how to authenticate with the API and retrieve data from it.
You can find all the source code for this on my GitHub repo.
Install Umami. You can find the installation instructions here this details how I install and use Umami to provide analytics for this site.
Again, this is a simple implementation of a few of the Umami Website Stats API endpoints. You can find the full API documentation here.
In this I have chose to implement the following endpoints:
GET /api/websites/:websiteId/pageviews
- As the name suggests, this endpoint returns the pageviews and 'sessions' for a given website over a time period.{
"pageviews": [
{ "x": "2020-04-20 01:00:00", "y": 3 },
{ "x": "2020-04-20 02:00:00", "y": 7 }
],
"sessions": [
{ "x": "2020-04-20 01:00:00", "y": 2 },
{ "x": "2020-04-20 02:00:00", "y": 4 }
]
}
GET /api/websites/:websiteId/stats
- this returns basic statistics for a given website.{
"pageviews": { "value": 5, "change": 5 },
"visitors": { "value": 1, "change": 1 },
"visits": { "value": 3, "change": 2 },
"bounces": { "value": 0, "change": 0 },
"totaltime": { "value": 4, "change": 4 }
}
GET /api/websites/:websiteId/metrics
- this returns the metrics for a given website bu URL etc...[
{ "x": "/", "y": 46 },
{ "x": "/docs", "y": 17 },
{ "x": "/download", "y": 14 }
]
As you can see from the docs, these all accept a number of parameters (and I have represented these as query parameters in the code below).
I always start by testing the API in Rider's built-in HTTP client. This allows me to quickly test the API and see the response.
### Login Request and Store Token
POST https://{{umamiurl}}/api/auth/login
Content-Type: application/json
{
"username": "{{username}}",
"password": "{{password}}"
}
> {% client.global.set("auth_token", response.body.token);
client.global.set("endAt", Math.round(new Date().getTime()).toString() );
client.global.set("startAt", Math.round(new Date().getTime() - 7 * 24 * 60 * 60 * 1000).toString());
%}
### Use Token in Subsequent Request
GET https://{{umamiurl}}/api/websites/{{websiteid}}/stats?endAt={{endAt}}&startAt={{startAt}}
Authorization: Bearer {{auth_token}}
### Use Token in Subsequent Request
GET https://{{umamiurl}}/api/websites/{{websiteid}}/pageviews?endAt={{endAt}}&startAt={{startAt}}&unit=day
Authorization: Bearer {{auth_token}}
###
GET https://{{umamiurl}}}}/api/websites/{{websiteid}}/metrics?endAt={{endAt}}&startAt={{startAt}}&type=url
Authorization: Bearer {{auth_token}}
It's good practice to keep the variable names here in {{}}
a env.json file which you can refer to like below.
{
"local": {
"umamiurl":"umamilocal.mostlylucid.net",
"username": "admin",
"password": "<password{>",
"websiteid" : "32c2aa31-b1ac-44c0-b8f3-ff1f50403bee"
}
}
First we need to configure out HttpClient and the services we will use to make the requests.
public static class UmamiSetup
{
public static void SetupUmamiServices(this IServiceCollection services, IConfiguration config)
{
var umamiSettings = services.ConfigurePOCO<UmamiSettings>(config.GetSection(UmamiSettings.Section));
services.AddHttpClient<AuthService>(options =>
{
options.BaseAddress = new Uri(umamiSettings.BaseUrl);
}) .SetHandlerLifetime(TimeSpan.FromMinutes(5)) //Set lifetime to five minutes
.AddPolicyHandler(GetRetryPolicy());;
services.AddScoped<UmamiService>();
services.AddScoped<AuthService>();
}
static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.OrResult(msg => msg.StatusCode == HttpStatusCode.ServiceUnavailable)
.WaitAndRetryAsync(6, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
}
}
Here we configure the settings class UmamiSettings
and add the AuthService
and UmamiService
to the services collection. We also add a retry policy to the HttpClient to handle transient errors.
Next we need to create the UmamiService
and AuthService
classes.
The AuthService
is simply responsible for getting the JWT token from the API.
public class AuthService(HttpClient httpClient, UmamiSettings umamiSettings, ILogger<AuthService> logger)
{
private string _token;
public HttpClient HttpClient => httpClient;
public async Task<bool> LoginAsync()
{
var loginData = new
{
username = umamiSettings.Username,
password = umamiSettings.Password
};
var content = new StringContent(JsonSerializer.Serialize(loginData), Encoding.UTF8, "application/json");
var response = await httpClient.PostAsync("/api/auth/login", content);
if (response.IsSuccessStatusCode)
{
var authResponse = await response.Content.ReadFromJsonAsync<AuthResponse>();
_token = authResponse.Token;
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token);
logger.LogInformation("Login successful");
return true;
}
logger.LogError("Login failed");
return false;
}
}
Here we have a simple method LoginAsync
that sends a POST request to the /api/auth/login
endpoint with the username and password. If the request is successful, we store the JWT token in the _token
field and set the Authorization
header on the HttpClient.
The UmamiService
is responsible for making the requests to the API.
For each of the main methods I've defined request objects which accept all the parameters for each endpoint. This makes it easier to test and maintain the code.
They all follow a simiiar pattern, so I'll just show one of them here.
public async Task<UmamiResult<StatsResponseModels>> GetStatsAsync(StatsRequest statsRequest)
{
// Start building the query string
var queryParams = new List<string>
{
$"start={statsRequest.StartAt}",
$"end={statsRequest.EndAt}"
};
// Add optional parameters if they are not null
if (!string.IsNullOrEmpty(statsRequest.Url)) queryParams.Add($"url={statsRequest.Url}");
if (!string.IsNullOrEmpty(statsRequest.Referrer)) queryParams.Add($"referrer={statsRequest.Referrer}");
if (!string.IsNullOrEmpty(statsRequest.Title)) queryParams.Add($"title={statsRequest.Title}");
if (!string.IsNullOrEmpty(statsRequest.Query)) queryParams.Add($"query={statsRequest.Query}");
if (!string.IsNullOrEmpty(statsRequest.Event)) queryParams.Add($"event={statsRequest.Event}");
if (!string.IsNullOrEmpty(statsRequest.Host)) queryParams.Add($"host={statsRequest.Host}");
if (!string.IsNullOrEmpty(statsRequest.Os)) queryParams.Add($"os={statsRequest.Os}");
if (!string.IsNullOrEmpty(statsRequest.Browser)) queryParams.Add($"browser={statsRequest.Browser}");
if (!string.IsNullOrEmpty(statsRequest.Device)) queryParams.Add($"device={statsRequest.Device}");
if (!string.IsNullOrEmpty(statsRequest.Country)) queryParams.Add($"country={statsRequest.Country}");
if (!string.IsNullOrEmpty(statsRequest.Region)) queryParams.Add($"region={statsRequest.Region}");
if (!string.IsNullOrEmpty(statsRequest.City)) queryParams.Add($"city={statsRequest.City}");
// Combine the query parameters into a query string
var queryString = string.Join("&", queryParams);
// Make the HTTP request
var response = await authService.HttpClient.GetAsync($"/api/websites/{WebsiteId}/stats?{queryString}");
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadFromJsonAsync<StatsResponseModels>();
return new UmamiResult<StatsResponseModels>(response.StatusCode, response.ReasonPhrase ?? "Success", content ?? new StatsResponseModels());
}
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
await authService.LoginAsync();
return await GetStatsAsync(statsRequest);
}
logger.LogError("Failed to get stats");
return new UmamiResult<StatsResponseModels>(response.StatusCode, response.ReasonPhrase ?? "Failed to get stats", null);
}
Here you can see I take the request object
public class BaseRequest
{
public long StartAt => StartAtDate.ToMilliseconds(); // Timestamp (in ms) of starting date
public long EndAt => EndAtDate.ToMilliseconds(); // Timestamp (in ms) of end date
public DateTime StartAtDate { get; set; }
public DateTime EndAtDate { get; set; }
}
public class StatsRequest : BaseRequest
{
// Optional properties
public string? Url { get; set; } // Name of URL
public string? Referrer { get; set; } // Name of referrer
public string? Title { get; set; } // Name of page title
public string? Query { get; set; } // Name of query
public string? Event { get; set; } // Name of event
public string? Host { get; set; } // Name of hostname
public string? Os { get; set; } // Name of operating system
public string? Browser { get; set; } // Name of browser
public string? Device { get; set; } // Name of device (e.g., Mobile)
public string? Country { get; set; } // Name of country
public string? Region { get; set; } // Name of region/state/province
public string? City { get; set; } // Name of city
}
And build the query string from the parameters. If the request is successful, we return the content as a UmamiResult
object. If the request fails with a 401 status code, we call the LoginAsync
method and retry the request. This ensures we 'elegantly' handle the token expiry.
This is a simple example of how to create a C# client for the Umami API. You can use this as a starting point to build more complex clients or integrate the API into your own applications.