diff --git a/Umami.Net.Test/Consts.cs b/Umami.Net.Test/Consts.cs index aba8f0e5..1103fc60 100644 --- a/Umami.Net.Test/Consts.cs +++ b/Umami.Net.Test/Consts.cs @@ -24,4 +24,6 @@ public class Consts public const string UserName = "Test User"; public const string SessionId = "B41A9964-FD33-4108-B6EC-9A6B68150763"; + + public const string DistinctId = "B41A9964-FD33-4108-B6EC-9A6B68150764"; } \ No newline at end of file diff --git a/Umami.Net.Test/UmamiBackgroundSender_Tests.cs b/Umami.Net.Test/UmamiBackgroundSender_Tests.cs index faa0e379..ed273b48 100644 --- a/Umami.Net.Test/UmamiBackgroundSender_Tests.cs +++ b/Umami.Net.Test/UmamiBackgroundSender_Tests.cs @@ -401,7 +401,7 @@ public async Task IdentifySession() var cancellationToken = new CancellationToken(); await hostedService.StartAsync(cancellationToken); - await backgroundSender.IdentifySession(sessionId, new UmamiEventData { { key, value } }); + await backgroundSender.IdentifySession(sessionId, eventData: new UmamiEventData { { key, value } }); var completedTask = await Task.WhenAny(tcs.Task, Task.Delay(1000, cancellationToken)); if (completedTask != tcs.Task) throw new TimeoutException("The background task did not complete in time."); diff --git a/Umami.Net.Test/UmamiClientTests/UmamiClient_IdentifyTests.cs b/Umami.Net.Test/UmamiClientTests/UmamiClient_IdentifyTests.cs index 98de48d0..fad20fdd 100644 --- a/Umami.Net.Test/UmamiClientTests/UmamiClient_IdentifyTests.cs +++ b/Umami.Net.Test/UmamiClientTests/UmamiClient_IdentifyTests.cs @@ -1,4 +1,6 @@ using System.Net.Http.Json; +using System.Text.Json; +using System.Text.Json.Serialization; using Umami.Net.Models; using Umami.Net.Test.Extensions; using Umami.Net.Test.MessageHandlers; @@ -60,6 +62,29 @@ public async Task Identify() Assert.Equal(Consts.UserId, content.Payload.Data["userId"].ToString()); } + [Fact] + public async Task IdentifySession_WithDistinctId_FlowsToPayloadIdField() + { + var umamiClient = SetupExtensions.GetUmamiClient(); + var response = await umamiClient.IdentifySession(Consts.SessionId, Consts.DistinctId); + + // Read response as string to validate JSON serialization + var responseString = await response.Content.ReadAsStringAsync(); + using var jsonDocument = JsonDocument.Parse(responseString); + var idField = jsonDocument.RootElement + .GetProperty("payload") + .GetProperty("id") + .GetString(); + Assert.Equal(Consts.DistinctId, idField); + + var content = await response.Content.ReadFromJsonAsync(); + Assert.NotNull(response); + Assert.NotNull(content); + Assert.NotNull(content.Payload); + Assert.Equal(Consts.DistinctId, content.Payload.DistinctId); + + } + private UmamiEventData BuildEventData(string? email, string? username, string? userId, UmamiEventData? eventData) { diff --git a/Umami.Net.Test/UmamiClientTests/UmamiClient_SendTests.cs b/Umami.Net.Test/UmamiClientTests/UmamiClient_SendTests.cs index 1e895e3f..9a42d93c 100644 --- a/Umami.Net.Test/UmamiClientTests/UmamiClient_SendTests.cs +++ b/Umami.Net.Test/UmamiClientTests/UmamiClient_SendTests.cs @@ -1,5 +1,9 @@ using System.Net; +using System.Net.Http.Json; +using System.Text.Json; +using Umami.Net.Models; using Umami.Net.Test.Extensions; +using Umami.Net.Test.MessageHandlers; namespace Umami.Net.Test.UmamiClientTests; @@ -19,4 +23,19 @@ public async Task Send_Empty_Success() var response = await umamiClient.Send(); Assert.Equal(HttpStatusCode.OK, response.StatusCode); } + + [Fact] + public async Task Send_WithDistinctId_FlowsToPayloadIdField() + { + var payload = new UmamiPayload { DistinctId = Consts.DistinctId }; + var umamiClient = SetupExtensions.GetUmamiClient(); + var response = await umamiClient.Send(payload); + + + var content = await response.Content.ReadFromJsonAsync(); + Assert.NotNull(response); + Assert.NotNull(content); + Assert.NotNull(content.Payload); + Assert.Equal(Consts.DistinctId, content.Payload.DistinctId); + } } \ No newline at end of file diff --git a/Umami.Net.Test/UmamiClientTests/UmamiClient_TrackTests.cs b/Umami.Net.Test/UmamiClientTests/UmamiClient_TrackTests.cs index aa214858..c12c0c56 100644 --- a/Umami.Net.Test/UmamiClientTests/UmamiClient_TrackTests.cs +++ b/Umami.Net.Test/UmamiClientTests/UmamiClient_TrackTests.cs @@ -1,5 +1,6 @@ using System.Net; using System.Net.Http.Json; +using System.Text.Json; using Umami.Net.Models; using Umami.Net.Test.Extensions; using Umami.Net.Test.MessageHandlers; @@ -65,4 +66,59 @@ public async Task Track_FullEvent() Assert.NotNull(content.Payload.Data); Assert.Equal("value", content.Payload.Data["string"].ToString()); } + + [Fact] + public async Task TrackPageView_WithDistinctId_FlowsToPayloadIdField() + { + var umamiClient = SetupExtensions.GetUmamiClient(); + + var response = await umamiClient.TrackPageView("https://example.com", "Example Page", distinctId: Consts.DistinctId); + + // Assert - Read response as string to validate JSON serialization + var responseString = await response.Content.ReadAsStringAsync(); + using var jsonDocument = JsonDocument.Parse(responseString); + + var idField = jsonDocument.RootElement + .GetProperty("payload") + .GetProperty("id") + .GetString(); + + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(Consts.DistinctId, idField); + + + var content = await response.Content.ReadFromJsonAsync(); + Assert.NotNull(response); + Assert.NotNull(content); + Assert.NotNull(content.Payload); + Assert.Equal(Consts.DistinctId, content.Payload.DistinctId); + } + + [Fact] + public async Task Track_WithDistinctId_FlowsToPayloadIdField() + { + var umamiClient = SetupExtensions.GetUmamiClient(); + + var response = await umamiClient.Track(Consts.DefaultName, distinctId: Consts.DistinctId); + + var responseString = await response.Content.ReadAsStringAsync(); + using var jsonDocument = JsonDocument.Parse(responseString); + + var idField = jsonDocument.RootElement + .GetProperty("payload") + .GetProperty("id") + .GetString(); + + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(Consts.DistinctId, idField); + + + var content = await response.Content.ReadFromJsonAsync(); + Assert.NotNull(response); + Assert.NotNull(content); + Assert.NotNull(content.Payload); + Assert.Equal(Consts.DistinctId, content.Payload.DistinctId); + } } \ No newline at end of file diff --git a/Umami.Net/Models/UmamiPayload.cs b/Umami.Net/Models/UmamiPayload.cs index 53e53f8f..83ec30a0 100644 --- a/Umami.Net/Models/UmamiPayload.cs +++ b/Umami.Net/Models/UmamiPayload.cs @@ -18,6 +18,8 @@ public class UmamiPayload public string? Name { get; set; } public string? SessionId { get; set; } + + [JsonPropertyName("id")] public string? DistinctId { get; set; } [JsonPropertyName("ip")] public string? IpAddress { get; set; } diff --git a/Umami.Net/PayloadService.cs b/Umami.Net/PayloadService.cs index 86e2884d..a0ca34b9 100644 --- a/Umami.Net/PayloadService.cs +++ b/Umami.Net/PayloadService.cs @@ -59,6 +59,8 @@ public UmamiPayload PopulateFromPayload(UmamiPayload? payload, UmamiEventData? d if (payload.SessionId != null) newPayload.SessionId = payload.SessionId; + if (payload.DistinctId != null) + newPayload.DistinctId = payload.DistinctId; newPayload.UserAgent = payload.UserAgent ?? DefaultUserAgent; diff --git a/Umami.Net/UmamiBackgroundSender.cs b/Umami.Net/UmamiBackgroundSender.cs index 579f9b66..24e3ea34 100644 --- a/Umami.Net/UmamiBackgroundSender.cs +++ b/Umami.Net/UmamiBackgroundSender.cs @@ -55,7 +55,7 @@ public async Task TrackPageView(string url, string title, UmamiPayload? payload public async Task Identify(string? email = null, string? username = null, string? sessionId = null, string? userId = null, UmamiEventData? eventData = null, - bool useDefaultUserAgent = false) + bool useDefaultUserAgent = false, string? distinctId = null) { await using var scope = scopeFactory.CreateAsyncScope(); eventData ??= new UmamiEventData(); @@ -71,6 +71,7 @@ public async Task Identify(string? email = null, string? username = null, { Data = eventData, SessionId = sessionId, + DistinctId = distinctId, UseDefaultUserAgent = useDefaultUserAgent }; var payloadService = scope.ServiceProvider.GetRequiredService(); @@ -79,12 +80,13 @@ public async Task Identify(string? email = null, string? username = null, } public async Task IdentifySession(string sessionId, UmamiEventData? eventData = null, - bool useDefaultUserAgent = false) + bool useDefaultUserAgent = false, string? distinctId = null) { await using var scope = scopeFactory.CreateAsyncScope(); var thisPayload = new UmamiPayload { SessionId = sessionId, + DistinctId = distinctId, Data = eventData ?? new UmamiEventData(), UseDefaultUserAgent = useDefaultUserAgent }; diff --git a/Umami.Net/UmamiClient.cs b/Umami.Net/UmamiClient.cs index e4954247..f03ba740 100644 --- a/Umami.Net/UmamiClient.cs +++ b/Umami.Net/UmamiClient.cs @@ -71,9 +71,10 @@ public async Task Send( string? url = "", string? title = "", UmamiPayload? payload = null, - UmamiEventData? eventData = null) + UmamiEventData? eventData = null, + string? distinctId = null) { - var response = await TrackPageView(url, title, payload, eventData); + var response = await TrackPageView(url, title, payload, eventData, distinctId); return await DecodeResponse(response); } @@ -82,7 +83,8 @@ public async Task TrackPageView( string? url = "", string? title = "", UmamiPayload? payload = null, - UmamiEventData? eventData = null) + UmamiEventData? eventData = null, + string? distinctId = null) { var sendPayload = payload ?? new UmamiPayload(); sendPayload.Data = eventData; @@ -90,44 +92,53 @@ public async Task TrackPageView( sendPayload.Url = url; if (!string.IsNullOrEmpty(title)) sendPayload.Title = title; + if (!string.IsNullOrEmpty(distinctId)) + sendPayload.DistinctId = distinctId; return await Send(sendPayload); } public async Task TrackAndDecode( string eventName, - UmamiEventData? eventData = null) + UmamiEventData? eventData = null, + string? distinctId = null) { - var response = await Track(eventName, eventData); + var response = await Track(eventName, eventData, distinctId); return await DecodeResponse(response); } public async Task TrackAndDecode( UmamiPayload eventObj, - UmamiEventData? eventData = null) + UmamiEventData? eventData = null, + string? distinctId = null) { - var response = await Track(eventObj, eventData); + var response = await Track(eventObj, eventData, distinctId); return await DecodeResponse(response); } public async Task Track( string eventName, - UmamiEventData? eventData = null) + UmamiEventData? eventData = null, + string? distinctId = null) { var thisPayload = new UmamiPayload { Name = eventName, - Data = eventData ?? new UmamiEventData() + Data = eventData ?? new UmamiEventData(), + DistinctId = distinctId }; return await Track(thisPayload); } public async Task Track(UmamiPayload eventObj, - UmamiEventData? eventData = null) + UmamiEventData? eventData = null, + string? distinctId = null) { var payload = eventObj; payload.Data = eventData ?? new UmamiEventData(); payload.Website = settings.WebsiteId; + if (!string.IsNullOrEmpty(distinctId)) + payload.DistinctId = distinctId; return await Send(payload); } @@ -172,9 +183,11 @@ public async Task Track(UmamiPayload eventObj, public async Task IdentifyAndDecode(string sessionId, string? email = null, string? username = null, - string? userId = null, UmamiEventData? eventData = null) + string? userId = null, + UmamiEventData? eventData = null, + string? distinctId = null) { - var response = await Identify(email, username, sessionId, userId, eventData); + var response = await Identify(email, username, sessionId, userId, eventData, distinctId); return await DecodeResponse(response); } @@ -186,21 +199,22 @@ public async Task Identify(UmamiPayload payload, UmamiEvent } public async Task Identify(string? email = null, string? username = null, - string? sessionId = null, string? userId = null, UmamiEventData? eventData = null) + string? sessionId = null, string? userId = null, UmamiEventData? eventData = null, string? distinctId = null) { var emailData = BuildEventData(email, username, userId, eventData); var payload = new UmamiPayload { SessionId = sessionId, + DistinctId = distinctId, Data = emailData }; return await Identify(payload, eventData); } - public async Task IdentifySession(string sessionId) + public async Task IdentifySession(string sessionId, string? distinctId = null) { - return await Identify(sessionId: sessionId); + return await Identify(sessionId: sessionId, distinctId: distinctId); } public async Task IdentifySessionAndDecode(string sessionId)