using System.Text.Json; using Kbx.Shared.Telemetry.Generated; using Kbx.Shared.Experiments.Generated; namespace Kbx.Shared.Telemetry; public sealed record KbxUxEventRequest( string EventName, string? ScreenId, string? ScreenVersion, Guid SessionId, Guid? TaskSessionId, string? AppVersion, string? ExperimentId, string? ExperimentVariant, DateTimeOffset OccurredAt, int? DurationMs, int? Count, IReadOnlyDictionary? Attributes); public sealed record KbxUxEventBatchRequest(IReadOnlyList Events); public sealed record KbxUxMetricRow(string MetricKey,string Label,double? Value,string Unit,long SampleCount,double? P50=null,double? P95=null); public sealed record KbxUxMetricsResponse(DateOnly From,DateOnly To,string? ScreenId,IReadOnlyList Metrics); public static class KbxUxTelemetryGuard { private static readonly string[] ForbiddenFragments = ["phone","address","name","orderno","customer","itemcode","barcode","keyword","query","email","businessnumber","entityid"]; public static void Validate(KbxUxEventRequest item) { if (!KbxTelemetryCatalog.Events.TryGetValue(item.EventName,out var definition)) throw new ArgumentException($"Unknown telemetry event: {item.EventName}"); if (definition.RequiresDuration && item.DurationMs is null) throw new ArgumentException($"{item.EventName} requires durationMs."); if (item.DurationMs is < 0 or > 86_400_000) throw new ArgumentOutOfRangeException(nameof(item.DurationMs)); if (item.ExperimentId is { Length: > 160 } || item.ExperimentVariant is { Length: > 80 }) throw new ArgumentException("Experiment context is too long."); if ((item.ExperimentId is null) != (item.ExperimentVariant is null)) throw new ArgumentException("Experiment id and variant must be supplied together."); if (item.Count is < 1 or > 1_000_000) throw new ArgumentOutOfRangeException(nameof(item.Count)); var allowed=definition.AllowedAttributes.ToHashSet(StringComparer.Ordinal); foreach(var pair in item.Attributes ?? new Dictionary()) { if(!allowed.Contains(pair.Key)) throw new ArgumentException($"Attribute '{pair.Key}' is not allowed for {item.EventName}."); if(ForbiddenFragments.Any(x=>pair.Key.Contains(x,StringComparison.OrdinalIgnoreCase))) throw new ArgumentException($"Attribute '{pair.Key}' is forbidden for telemetry."); if(pair.Value.Length>80) throw new ArgumentException($"Attribute value is too long: {pair.Key}"); } } }