diff --git a/src/NexusMods.Telemetry/EventDefinition.cs b/src/NexusMods.Telemetry/EventDefinition.cs
index fed9432a7..56c879fcf 100644
--- a/src/NexusMods.Telemetry/EventDefinition.cs
+++ b/src/NexusMods.Telemetry/EventDefinition.cs
@@ -1,3 +1,5 @@
+using System.Net;
+using System.Text;
using JetBrains.Annotations;
namespace NexusMods.Telemetry;
@@ -8,4 +10,14 @@ namespace NexusMods.Telemetry;
/// The event category
/// The event action
[PublicAPI]
-public record EventDefinition(string Category, string Action);
+public record EventDefinition(string Category, string Action)
+{
+ internal byte[] SafeCategory { get; } = Encode(Category);
+ internal byte[] SafeAction { get; } = Encode(Action);
+
+ private static byte[] Encode(string value)
+ {
+ var bytes = Encoding.UTF8.GetBytes(value);
+ return WebUtility.UrlEncodeToBytes(bytes, offset: 0, count: bytes.Length);
+ }
+};
diff --git a/src/NexusMods.Telemetry/EventMetadata.cs b/src/NexusMods.Telemetry/EventMetadata.cs
index c680d1dd4..ec2246dab 100644
--- a/src/NexusMods.Telemetry/EventMetadata.cs
+++ b/src/NexusMods.Telemetry/EventMetadata.cs
@@ -1,4 +1,6 @@
using System.Diagnostics;
+using System.Net;
+using System.Text;
using JetBrains.Annotations;
namespace NexusMods.Telemetry;
@@ -41,4 +43,12 @@ public EventMetadata(string? name, TimeProvider? timeProvider = null)
/// Checks whether the struct wasn't default initialized.
///
public bool IsValid() => Name is not null || CurrentTime != default(TimeOnly);
+
+ internal byte[] SafeName => Name is null ? [] : Encode(Name);
+
+ private static byte[] Encode(string value)
+ {
+ var bytes = Encoding.UTF8.GetBytes(value);
+ return WebUtility.UrlEncodeToBytes(bytes, offset: 0, count: bytes.Length);
+ }
}
diff --git a/src/NexusMods.Telemetry/EventSender.cs b/src/NexusMods.Telemetry/EventSender.cs
index a29bd12a5..3ffbf1065 100644
--- a/src/NexusMods.Telemetry/EventSender.cs
+++ b/src/NexusMods.Telemetry/EventSender.cs
@@ -163,14 +163,29 @@ private static void SerializeEvent(IBufferWriter writer, ValueTuple