Skip to content

Commit 3922429

Browse files
committed
add telemetry and update libs (#212)
* minor tweaks for wiki url * fix order points not multiplying in item mode * add telemetry service * remove warning
1 parent 2257085 commit 3922429

20 files changed

Lines changed: 359 additions & 29 deletions

File tree

.github/workflows/build.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,12 @@ jobs:
141141
shell: bash
142142
run: |
143143
echo "CopyrightYear=$(date +%Y)" >> $GITHUB_ENV
144+
145+
- name: Inject telemetry key
146+
shell: bash
147+
run: |
148+
sed -i 's/BUILD_TELEMETRY_KEY/${{ secrets.TELEMETRY_KEY }}/g' \
149+
SubathonManager.Services/TelemetryService.cs
144150
145151
- name: Build project
146152
run: >

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424

2525
## [Download Latest](https://github.com/WolfwithSword/SubathonManager/releases/latest)
2626

27-
### [Check the Wiki](https://wolfwithsword.github.io/SubathonManager-docs)
27+
### [Check the Wiki](https://docs.subathonmanager.app)
2828

2929

3030
## Supported Integrations

SubathonManager.Core/Config.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,5 +165,16 @@ public bool SetEncoded(string section, string key, string value)
165165
var bytes = System.Text.Encoding.UTF8.GetBytes(value);
166166
return Set(section, key, Convert.ToBase64String(bytes));
167167
}
168+
169+
public string GetInstallId()
170+
{
171+
var id = Get("Telemetry", "InstallId", "");
172+
if (!string.IsNullOrWhiteSpace(id)) return id;
173+
174+
id = Guid.NewGuid().ToString();
175+
Set("Telemetry", "InstallId", id);
176+
Save();
177+
return id;
178+
}
168179
}
169180
}

SubathonManager.Core/Interfaces/IConfig.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,6 @@ public interface IConfig
1818

1919
bool SetBool(string section, string key, bool? value);
2020
bool SetEncoded(string section, string key, string value);
21+
22+
string GetInstallId();
2123
}

SubathonManager.Core/Utils.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ public static class Utils
1515
public static readonly Dictionary<string, bool> DonationSettings = new();
1616
private static readonly ConcurrentDictionary<(SubathonEventSource Source, string Service), IntegrationConnection> ConnectionDetails = new();
1717

18+
19+
public static IEnumerable<IntegrationConnection> GetAllConnections()
20+
=> ConnectionDetails.Values;
1821
public static IntegrationConnection GetConnection(SubathonEventSource source, string service)
1922
{
2023
ConnectionDetails.TryGetValue((source, service), out var conn);

SubathonManager.Integration/SubathonManager.Integration.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
</PropertyGroup>
1111

1212
<ItemGroup>
13-
<PackageReference Include="Agash.YTLiveChat" Version="4.0.0" />
14-
<PackageReference Include="GoAffPro.Client" Version="0.4.0-dev1.1" />
13+
<PackageReference Include="Agash.YTLiveChat" Version="4.1.0" />
14+
<PackageReference Include="GoAffPro.Client" Version="0.4.0" />
1515
<PackageReference Include="StreamElements.WebSocket" Version="1.0.2" />
1616
<PackageReference Include="Streamlabs.SocketClient" Version="3.0.0" />
1717
<PackageReference Include="System.Net.Security" Version="4.3.2" />

SubathonManager.Integration/YouTubeService.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ private void OnChatReceived(object? sender, ChatReceivedEventArgs e)
273273
subathonEvent.Value = tier;
274274
subathonEvent.Currency = "member";
275275

276-
if (tier.ToLower().Contains("new") && details.EventType == MembershipEventType.Unknown)
276+
if (tier.Contains("new", StringComparison.CurrentCultureIgnoreCase) && details.EventType == MembershipEventType.Unknown)
277277
details.EventType = MembershipEventType.New;
278278

279279
switch (details.EventType)
@@ -306,6 +306,13 @@ private void OnChatReceived(object? sender, ChatReceivedEventArgs e)
306306

307307
if (tier.Equals("Member", StringComparison.OrdinalIgnoreCase) || string.IsNullOrWhiteSpace(tier))
308308
tier = "DEFAULT";
309+
if (tier.StartsWith("Upgraded membership to")) // upgrade type
310+
{
311+
// levelname might be correct here, but incredibly hard to test
312+
tier = tier.Replace("Upgraded membership to", "");
313+
tier = tier[..^1]; // remove end !
314+
315+
}
309316
subathonEvent.Value = tier.Trim();
310317

311318
SubathonEvents.RaiseSubathonEventCreated(subathonEvent);

SubathonManager.Services/EventService.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -167,8 +167,6 @@ private async Task LoopAsync()
167167
{
168168
subathonValue = await db.SubathonValues.FirstOrDefaultAsync(v =>
169169
v.EventType == ev.EventType && (v.Meta.ToLower() == ev.Value.ToLower() ||
170-
//v.Meta.ToLower().Replace("!", string.Empty) == ev.Value.ToLower().Replace("!", string.Empty) ||
171-
// Youtube has an issue where memberships with ! in it will include everything before the ! and nother else
172170
v.Meta == string.Empty));
173171

174172
subathonValue ??= await db.SubathonValues.FirstOrDefaultAsync(v =>
@@ -190,9 +188,10 @@ private async Task LoopAsync()
190188
&& !_currencyService.IsValidCurrency(ev.Currency)
191189
&& !string.IsNullOrEmpty(ev.Value.Trim()))
192190
{
193-
ev.SecondsValue = parsedValue * subathonValue.Seconds;
191+
ev.SecondsValue = parsedValue * subathonValue.Seconds; // also handles tokens, but points overwritten later
192+
ev.PointsValue = (int?)(parsedValue * Math.Floor(subathonValue!.Points)); // i went through 21k+ lines of changes to find this missing
194193
}
195-
else if (!string.IsNullOrEmpty(ev.Currency) && "sub,member,viewers,bits,beets,order".Split(",").Contains(ev.Currency)) // flat orders
194+
else if (!string.IsNullOrEmpty(ev.Currency) && "sub,member,viewers,bits,beets,order".Split(",").Contains(ev.Currency.ToLower())) // flat orders
196195
{
197196
ev.SecondsValue = subathonValue.Seconds;
198197
}
@@ -220,6 +219,7 @@ private async Task LoopAsync()
220219

221220
if (ev.EventType.IsToken() && double.TryParse(ev.Value, out var parsedBitsLikeValue))
222221
{
222+
// seconds in sub value are stored as 0.12 so it is done above
223223
ev.PointsValue = (int) Math.Floor(((parsedBitsLikeValue / 100)) * subathonValue!.Points);
224224
}
225225
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
using System.Net.Http.Json;
3+
using Microsoft.Extensions.Logging;
4+
using SubathonManager.Core;
5+
using SubathonManager.Core.Interfaces;
6+
7+
namespace SubathonManager.Services;
8+
9+
[ExcludeFromCodeCoverage]
10+
public class TelemetryService(ILogger<TelemetryService>? logger, IConfig config): IDisposable, IAppService
11+
{
12+
private readonly HttpClient _http = new();
13+
private static readonly TimeSpan PingInterval = TimeSpan.FromHours(1);
14+
private DateTime LastPinged { get; set; } = DateTime.MinValue;
15+
private readonly string _telemetryUrl = "https://telemetry.subathonmanager.app";
16+
private CancellationTokenSource? _cts;
17+
private Task? _loopTask;
18+
private static string? TelemetryKey =>
19+
"BUILD_TELEMETRY_KEY".StartsWith("BUILD") ? Environment.GetEnvironmentVariable("SM_TELEMETRY_SECRET") : "BUILD_TELEMETRY_KEY";
20+
21+
private async Task TryPingAsync()
22+
{
23+
if (!config.GetBool("Telemetry", "Enabled", false)) return;
24+
25+
if (DateTime.UtcNow - LastPinged < PingInterval) return;
26+
27+
try
28+
{
29+
LastPinged = DateTime.UtcNow;
30+
await Task.Delay(5000); // offset a bit
31+
var connections = Utils.GetAllConnections().Where(x => x.Service != "Unknown")
32+
.Select(c => new
33+
{
34+
source = c.Source.ToString(),
35+
service = c.Service,
36+
connected = c.Status
37+
});
38+
39+
var payload = new
40+
{
41+
install_id = config.GetInstallId(),
42+
version = AppServices.AppVersion,
43+
connections
44+
};
45+
46+
var request = new HttpRequestMessage(HttpMethod.Post, _telemetryUrl);
47+
request.Headers.Add("X-Telemetry-Key", TelemetryKey);
48+
request.Content = JsonContent.Create(payload);
49+
50+
var response = await _http.SendAsync(request);
51+
//LastPinged = DateTime.UtcNow;
52+
logger?.LogDebug("Telemetry sent for install id {InstallId}. Response: {ResponseCode}", config.GetInstallId(), response.StatusCode);
53+
}
54+
catch { /**/ }
55+
}
56+
57+
58+
private async Task RunLoopAsync(CancellationToken ct)
59+
{
60+
while (!ct.IsCancellationRequested)
61+
{
62+
await TryPingAsync();
63+
try
64+
{
65+
await Task.Delay(PingInterval, ct);
66+
}
67+
catch (OperationCanceledException)
68+
{
69+
break;
70+
}
71+
}
72+
}
73+
74+
public void Dispose()
75+
{
76+
_cts?.Cancel();
77+
_cts?.Dispose();
78+
}
79+
80+
public Task StartAsync(CancellationToken ct = default)
81+
{
82+
LastPinged = DateTime.MinValue;
83+
_cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
84+
_loopTask = RunLoopAsync(_cts.Token);
85+
return Task.CompletedTask;
86+
}
87+
88+
public Task StopAsync(CancellationToken ct = default)
89+
{
90+
_cts?.Cancel();
91+
return _loopTask ?? Task.CompletedTask;
92+
}
93+
}

SubathonManager.Tests/IntegrationUnitTests/YouTubeServiceTests.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -772,12 +772,13 @@ public void OnChatReceived_Membership_IdHandling(bool isValidGuid)
772772
}
773773

774774
[Theory]
775-
[InlineData(MembershipEventType.New, "Gold", "New Member", SubathonEventType.YouTubeMembership)]
776-
[InlineData(MembershipEventType.Milestone, "DEFAULT", "Special guy", SubathonEventType.YouTubeMembership)]
777-
[InlineData(MembershipEventType.Unknown, "Gold", "Some subtext", SubathonEventType.YouTubeMembership)]
778-
[InlineData(MembershipEventType.Unknown, "Gold", "Welcome new member!", SubathonEventType.YouTubeMembership)]
775+
[InlineData(MembershipEventType.New, "Gold", "New Member", SubathonEventType.YouTubeMembership, "Gold")]
776+
[InlineData(MembershipEventType.Milestone, "DEFAULT", "Special guy", SubathonEventType.YouTubeMembership, "DEFAULT")]
777+
[InlineData(MembershipEventType.Unknown, "Gold", "Some subtext", SubathonEventType.YouTubeMembership, "Some subtext")] // not true case
778+
[InlineData(MembershipEventType.Unknown, "Gold", "Welcome new member!", SubathonEventType.YouTubeMembership, "Gold")]
779+
[InlineData(MembershipEventType.Unknown, "Upgraded membership to MyTier!!", "Upgraded membership to MyTier!!", SubathonEventType.YouTubeMembership, "MyTier!")]
779780
public void OnChatReceived_Membership_RaisesYouTubeMembership(
780-
MembershipEventType eventType, string levelName, string headerSubtext, SubathonEventType expectedEventType)
781+
MembershipEventType eventType, string levelName, string headerSubtext, SubathonEventType expectedEventType, string expectedLevel)
781782
{
782783
var logger = new Mock<ILogger<YouTubeService>>();
783784
var chatLogger = new Mock<ILogger<YTLiveChat.Services.YTLiveChat>>();
@@ -813,6 +814,7 @@ public void OnChatReceived_Membership_RaisesYouTubeMembership(
813814
Assert.NotNull(captured);
814815
Assert.Equal(expectedEventType, captured!.EventType);
815816
Assert.Equal(SubathonEventSource.YouTube, captured.Source);
817+
Assert.Equal(expectedLevel, captured.Value);
816818
Assert.Equal("MemberUser", captured.User);
817819
}
818820

0 commit comments

Comments
 (0)