Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions unity-sdk/Runtime/Providers/ConfidenceApiClient.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
Expand All @@ -9,7 +10,9 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using UnityEngine.Scripting;
using UnityOpenFeature.Telemetry;
using Object = UnityEngine.Object;
using Debug = UnityEngine.Debug;

namespace UnityOpenFeature.Providers
{
Expand Down Expand Up @@ -41,6 +44,9 @@ public class ConfidenceApiClient : MonoBehaviour
private float checkpointTimer = 0f;
private const float CHECKPOINT_INTERVAL = 10f; // 10 seconds

private const string TelemetryHeaderName = "X-CONFIDENCE-TELEMETRY";
internal Telemetry.Telemetry Telemetry { get; private set; }

// Private constructor - use Create() method instead
private ConfidenceApiClient() { }

Expand All @@ -53,6 +59,7 @@ public static ConfidenceApiClient Create(string clientSecret)
// Add the client as a component
ConfidenceApiClient client = clientGO.AddComponent<ConfidenceApiClient>();
client.clientSecret = clientSecret;
client.Telemetry = new Telemetry.Telemetry(Platform.DotNet, client.sdkVersion);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be unity?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes!


return client;
}
Expand Down Expand Up @@ -103,12 +110,32 @@ public async Task ResolveFlagsAsync(List<string> flagKeys, Dictionary<string, ob
};

string jsonBody = JsonConvert.SerializeObject(requestBody);
var stopwatch = Stopwatch.StartNew();
RequestStatus resolveStatus = RequestStatus.Ok;

using (UnityWebRequest request = new UnityWebRequest(url, "POST"))
{
// Set headers
request.SetRequestHeader("Content-Type", "application/json");
request.SetRequestHeader("Accept", "application/json");

// Attach telemetry header
try
{
if (Telemetry != null)
{
var headerValue = Telemetry.EncodedHeaderValue();
if (headerValue != null)
{
request.SetRequestHeader(TelemetryHeaderName, headerValue);
}
}
}
catch (Exception ex)
{
Debug.Log($"Telemetry header error (best-effort): {ex.Message}");
}

request.downloadHandler = new DownloadHandlerBuffer();

// Set upload handler with JSON body
Expand All @@ -120,6 +147,8 @@ public async Task ResolveFlagsAsync(List<string> flagKeys, Dictionary<string, ob
await Task.Delay(100); // Small delay to prevent busy waiting
}

stopwatch.Stop();

if (request.result == UnityWebRequest.Result.Success)
{
try
Expand All @@ -133,15 +162,27 @@ public async Task ResolveFlagsAsync(List<string> flagKeys, Dictionary<string, ob
}
catch (Exception ex)
{
resolveStatus = RequestStatus.Error;
callback?.Invoke(null, $"Failed to parse response: {ex.Message}");
}
}
else
{
resolveStatus = RequestStatus.Error;
string errorMsg = $"Network request failed: {request.error}";
callback?.Invoke(null, errorMsg);
}
}

// Track resolve latency (best-effort)
try
{
Telemetry?.TrackResolveLatency((ulong)stopwatch.ElapsedMilliseconds, resolveStatus);
}
catch (Exception ex)
{
Debug.Log($"Telemetry latency tracking error (best-effort): {ex.Message}");
}
}

public void ApplyFlag(string flagKey, string resolveToken)
Expand Down
26 changes: 25 additions & 1 deletion unity-sdk/Runtime/Providers/ConfidenceProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using UnityEngine.Networking;
using Newtonsoft.Json;
using UnityOpenFeature.Core;
using UnityOpenFeature.Telemetry;

namespace UnityOpenFeature.Providers
{
Expand Down Expand Up @@ -294,7 +295,11 @@ public ResolutionDetails<T> ResolveObjectValue<T>(string flagKey, T defaultValue
{
var value = ResolveValueByDotNotation(flagKey);
if (value == null)
return ResolutionDetails<T>.Error(flagKey, defaultValue, ErrorCode.FlagNotFound, $"Flag '{flagKey}' not found");
{
var errorResult = ResolutionDetails<T>.Error(flagKey, defaultValue, ErrorCode.FlagNotFound, $"Flag '{flagKey}' not found");
TrackEvaluation(null, errorResult.ErrorMessage);
return errorResult;
}

var rootFlagKey = flagKey.Split('.')[0];
var resolvedFlag = GetResolvedFlag(rootFlagKey);
Expand All @@ -315,6 +320,8 @@ public ResolutionDetails<T> ResolveObjectValue<T>(string flagKey, T defaultValue
catch (Exception ex)
{
details = ResolutionDetails<T>.Error(flagKey, defaultValue, ErrorCode.ParseError, $"Cannot parse: {ex.Message}");
TrackEvaluation(resolvedFlag?.reason, details.ErrorMessage);
return details;
}

if (resolvedFlag != null)
Expand All @@ -325,6 +332,7 @@ public ResolutionDetails<T> ResolveObjectValue<T>(string flagKey, T defaultValue
tryApply(resolvedFlag, rootFlagKey);
}

TrackEvaluation(resolvedFlag?.reason, null);
return details;
}

Expand Down Expand Up @@ -397,6 +405,22 @@ private Reason MapResolveReasonToReason(string resolveReason)
};
}

private void TrackEvaluation(string apiReason, string errorMessage)
{
try
{
if (apiClient?.Telemetry != null)
{
var (reason, errorCode) = Telemetry.Telemetry.MapEvaluationReason(apiReason, errorMessage);
apiClient.Telemetry.TrackEvaluation(reason, errorCode);
}
}
catch (Exception ex)
{
Debug.Log($"Telemetry eval tracking error (best-effort): {ex.Message}");
}
}

private Dictionary<string, object> GetEvaluationContext()
{
var context = OpenFeatureAPI.Instance.EvaluationContext;
Expand Down
175 changes: 175 additions & 0 deletions unity-sdk/Runtime/Telemetry/ProtobufEncoder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
using System.Collections.Generic;
using System.IO;
using System.Text;

namespace UnityOpenFeature.Telemetry
{
internal static class ProtobufEncoder
{
private const int WireTypeVarint = 0;
private const int WireTypeLengthDelimited = 2;

public static byte[] EncodeMonitoring(
Library library,
string sdkVersion,
Platform platform,
IReadOnlyList<ResolveLatencyTraceData> resolveTraces,
IReadOnlyList<EvaluationTraceData> evalTraces)
{
using (var ms = new MemoryStream())
{
var libraryTracesBytes = EncodeLibraryTraces(library, sdkVersion, resolveTraces, evalTraces);
if (libraryTracesBytes.Length > 0)
{
WriteLengthDelimited(ms, 1, libraryTracesBytes);
}

if (platform != Platform.Unspecified)
{
WriteTag(ms, 2, WireTypeVarint);
WriteVarint(ms, (ulong)platform);
}

return ms.ToArray();
}
}

private static byte[] EncodeLibraryTraces(
Library library,
string sdkVersion,
IReadOnlyList<ResolveLatencyTraceData> resolveTraces,
IReadOnlyList<EvaluationTraceData> evalTraces)
{
using (var ms = new MemoryStream())
{
if (library != Library.Unspecified)
{
WriteTag(ms, 1, WireTypeVarint);
WriteVarint(ms, (ulong)library);
}

if (!string.IsNullOrEmpty(sdkVersion))
{
var versionBytes = Encoding.UTF8.GetBytes(sdkVersion);
WriteLengthDelimited(ms, 2, versionBytes);
}

foreach (var trace in resolveTraces)
{
var traceBytes = EncodeResolveTrace(trace);
WriteLengthDelimited(ms, 3, traceBytes);
}

foreach (var trace in evalTraces)
{
var traceBytes = EncodeEvaluationTrace(trace);
WriteLengthDelimited(ms, 3, traceBytes);
}

return ms.ToArray();
}
}

private static byte[] EncodeResolveTrace(ResolveLatencyTraceData data)
{
using (var ms = new MemoryStream())
{
WriteTag(ms, 1, WireTypeVarint);
WriteVarint(ms, (ulong)TraceId.ResolveLatency);

var requestTraceBytes = EncodeRequestTrace(data);
if (requestTraceBytes.Length > 0)
{
WriteLengthDelimited(ms, 3, requestTraceBytes);
}

return ms.ToArray();
}
}

private static byte[] EncodeRequestTrace(ResolveLatencyTraceData data)
{
using (var ms = new MemoryStream())
{
if (data.MillisecondDuration > 0)
{
WriteTag(ms, 1, WireTypeVarint);
WriteVarint(ms, data.MillisecondDuration);
}

if (data.Status != RequestStatus.Unspecified)
{
WriteTag(ms, 2, WireTypeVarint);
WriteVarint(ms, (ulong)data.Status);
}

return ms.ToArray();
}
}

private static byte[] EncodeEvaluationTrace(EvaluationTraceData data)
{
using (var ms = new MemoryStream())
{
WriteTag(ms, 1, WireTypeVarint);
WriteVarint(ms, (ulong)TraceId.EvaluationOutcome);

var evalTraceBytes = EncodeEvalTraceBody(data);
if (evalTraceBytes.Length > 0)
{
WriteLengthDelimited(ms, 5, evalTraceBytes);
}

return ms.ToArray();
}
}

private static byte[] EncodeEvalTraceBody(EvaluationTraceData data)
{
using (var ms = new MemoryStream())
{
if (data.Reason != EvaluationReason.Unspecified)
{
WriteTag(ms, 1, WireTypeVarint);
WriteVarint(ms, (ulong)data.Reason);
}

if (data.ErrorCode != EvaluationErrorCode.Unspecified)
{
WriteTag(ms, 2, WireTypeVarint);
WriteVarint(ms, (ulong)data.ErrorCode);
}

return ms.ToArray();
}
}

private static void WriteTag(Stream stream, int fieldNumber, int wireType)
{
WriteVarint(stream, (ulong)((fieldNumber << 3) | wireType));
}

private static void WriteVarint(Stream stream, ulong value)
{
do
{
var b = (byte)(value & 0x7F);
value >>= 7;
if (value != 0)
{
b |= 0x80;
}

stream.WriteByte(b);
}
while (value != 0);
}

private static void WriteLengthDelimited(Stream stream, int fieldNumber, byte[] data)
{
WriteTag(stream, fieldNumber, WireTypeLengthDelimited);
WriteVarint(stream, (ulong)data.Length);
stream.Write(data, 0, data.Length);
}
}
}
Loading
Loading