diff --git a/src/OpenFeature/Telemetry/EvaluationEvent.cs b/src/OpenFeature/Telemetry/EvaluationEvent.cs
new file mode 100644
index 00000000..51506ad7
--- /dev/null
+++ b/src/OpenFeature/Telemetry/EvaluationEvent.cs
@@ -0,0 +1,30 @@
+using System.Collections.Generic;
+
+namespace OpenFeature.Telemetry;
+
+///
+/// Represents an evaluation event for feature flags.
+///
+public class EvaluationEvent
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The name of the event.
+ /// The attributes of the event.
+ public EvaluationEvent(string name, IDictionary attributes)
+ {
+ Name = name;
+ Attributes = new Dictionary(attributes);
+ }
+
+ ///
+ /// Gets the name of the event.
+ ///
+ public string Name { get; }
+
+ ///
+ /// Gets the attributes of the event.
+ ///
+ public IReadOnlyDictionary Attributes { get; }
+}
diff --git a/src/OpenFeature/Telemetry/EvaluationEventBuilder.cs b/src/OpenFeature/Telemetry/EvaluationEventBuilder.cs
new file mode 100644
index 00000000..2f73224f
--- /dev/null
+++ b/src/OpenFeature/Telemetry/EvaluationEventBuilder.cs
@@ -0,0 +1,58 @@
+using System.Collections.Generic;
+using OpenFeature.Constant;
+using OpenFeature.Model;
+
+namespace OpenFeature.Telemetry;
+
+///
+/// Class for creating evaluation events for feature flags.
+///
+public sealed class EvaluationEventBuilder
+{
+ private const string EventName = "feature_flag.evaluation";
+
+ ///
+ /// Gets the default instance of the .
+ ///
+ public static EvaluationEventBuilder Default { get; } = new();
+
+ ///
+ /// Creates an evaluation event based on the provided hook context and flag evaluation details.
+ ///
+ /// The context of the hook containing flag key and provider metadata.
+ /// The details of the flag evaluation including reason, variant, and metadata.
+ /// An instance of containing the event name, attributes, and body.
+ public EvaluationEvent Build(HookContext hookContext, FlagEvaluationDetails details)
+ {
+ var attributes = new Dictionary
+ {
+ { TelemetryConstants.Key, hookContext.FlagKey },
+ { TelemetryConstants.Provider, hookContext.ProviderMetadata.Name }
+ };
+
+ attributes[TelemetryConstants.Reason] = !string.IsNullOrWhiteSpace(details.Reason)
+ ? details.Reason?.ToLowerInvariant()
+ : Reason.Unknown.ToLowerInvariant();
+ attributes[TelemetryConstants.Variant] = details.Variant;
+ attributes[TelemetryConstants.Value] = details.Value;
+
+ if (details.FlagMetadata != null)
+ {
+ attributes[TelemetryConstants.ContextId] = details.FlagMetadata.GetString(TelemetryFlagMetadata.ContextId);
+ attributes[TelemetryConstants.FlagSetId] = details.FlagMetadata.GetString(TelemetryFlagMetadata.FlagSetId);
+ attributes[TelemetryConstants.Version] = details.FlagMetadata.GetString(TelemetryFlagMetadata.Version);
+ }
+
+ if (details.ErrorType != ErrorType.None)
+ {
+ attributes[TelemetryConstants.ErrorCode] = details.ErrorType.ToString().ToLowerInvariant();
+
+ if (!string.IsNullOrWhiteSpace(details.ErrorMessage))
+ {
+ attributes[TelemetryConstants.ErrorMessage] = details.ErrorMessage;
+ }
+ }
+
+ return new EvaluationEvent(EventName, attributes);
+ }
+}
diff --git a/src/OpenFeature/Telemetry/TelemetryConstants.cs b/src/OpenFeature/Telemetry/TelemetryConstants.cs
new file mode 100644
index 00000000..62730666
--- /dev/null
+++ b/src/OpenFeature/Telemetry/TelemetryConstants.cs
@@ -0,0 +1,59 @@
+namespace OpenFeature.Telemetry;
+
+///
+/// The attributes of an OpenTelemetry compliant event for flag evaluation.
+///
+///
+public static class TelemetryConstants
+{
+ ///
+ /// The lookup key of the feature flag.
+ ///
+ public const string Key = "feature_flag.key";
+
+ ///
+ /// Describes a class of error the operation ended with.
+ ///
+ public const string ErrorCode = "error.type";
+
+ ///
+ /// A message explaining the nature of an error occurring during flag evaluation.
+ ///
+ public const string ErrorMessage = "error.message";
+
+ ///
+ /// A semantic identifier for an evaluated flag value.
+ ///
+ public const string Variant = "feature_flag.result.variant";
+
+ ///
+ /// The evaluated value of the feature flag.
+ ///
+ public const string Value = "feature_flag.result.value";
+
+ ///
+ /// The unique identifier for the flag evaluation context. For example, the targeting key.
+ ///
+ public const string ContextId = "feature_flag.context.id";
+
+ ///
+ /// The reason code which shows how a feature flag value was determined.
+ ///
+ public const string Reason = "feature_flag.result.reason";
+
+ ///
+ /// Describes a class of error the operation ended with.
+ ///
+ public const string Provider = "feature_flag.provider.name";
+
+ ///
+ /// The identifier of the flag set to which the feature flag belongs.
+ ///
+ public const string FlagSetId = "feature_flag.set.id";
+
+ ///
+ /// The version of the ruleset used during the evaluation. This may be any stable value which uniquely identifies the ruleset.
+ ///
+ public const string Version = "feature_flag.version";
+
+}
diff --git a/src/OpenFeature/Telemetry/TelemetryFlagMetadata.cs b/src/OpenFeature/Telemetry/TelemetryFlagMetadata.cs
new file mode 100644
index 00000000..40b58f04
--- /dev/null
+++ b/src/OpenFeature/Telemetry/TelemetryFlagMetadata.cs
@@ -0,0 +1,25 @@
+namespace OpenFeature.Telemetry;
+
+///
+/// Well-known flag metadata attributes for telemetry events.
+/// See also: https://openfeature.dev/specification/appendix-d#flag-metadata
+///
+public static class TelemetryFlagMetadata
+{
+ ///
+ /// The context identifier returned in the flag metadata uniquely identifies
+ /// the subject of the flag evaluation. If not available, the targeting key
+ /// should be used.
+ ///
+ public const string ContextId = "contextId";
+
+ ///
+ /// ///A logical identifier for the flag set.
+ ///
+ public const string FlagSetId = "flagSetId";
+
+ ///
+ /// A version string (format unspecified) for the flag or flag set.
+ ///
+ public const string Version = "version";
+}
diff --git a/test/OpenFeature.Tests/Telemetry/EvaluationEventBuilderTests.cs b/test/OpenFeature.Tests/Telemetry/EvaluationEventBuilderTests.cs
new file mode 100644
index 00000000..3b02a8ee
--- /dev/null
+++ b/test/OpenFeature.Tests/Telemetry/EvaluationEventBuilderTests.cs
@@ -0,0 +1,174 @@
+using System.Collections.Generic;
+using OpenFeature.Constant;
+using OpenFeature.Model;
+using OpenFeature.Telemetry;
+using Xunit;
+
+namespace OpenFeature.Tests.Telemetry;
+
+public class EvaluationEventBuilderTests
+{
+ private readonly EvaluationEventBuilder _builder = EvaluationEventBuilder.Default;
+
+ [Fact]
+ public void Build_ShouldReturnEventWithCorrectAttributes()
+ {
+ // Arrange
+ var clientMetadata = new ClientMetadata("client", "1.0.0");
+ var providerMetadata = new Metadata("provider");
+ var hookContext = new HookContext("flagKey", new Value(), FlagValueType.Object, clientMetadata,
+ providerMetadata, EvaluationContext.Empty);
+ var metadata = new Dictionary
+ {
+ { "flagSetId", "flagSetId" }, { "contextId", "contextId" }, { "version", "version" }
+ };
+ var flagMetadata = new ImmutableMetadata(metadata);
+ var details = new FlagEvaluationDetails("flagKey", new Value("value"), ErrorType.None,
+ reason: "reason", variant: "variant", flagMetadata: flagMetadata);
+
+ // Act
+ var evaluationEvent = _builder.Build(hookContext, details);
+
+ // Assert
+ Assert.Equal("feature_flag.evaluation", evaluationEvent.Name);
+ Assert.Equal("flagKey", evaluationEvent.Attributes[TelemetryConstants.Key]);
+ Assert.Equal("provider", evaluationEvent.Attributes[TelemetryConstants.Provider]);
+ Assert.Equal("reason", evaluationEvent.Attributes[TelemetryConstants.Reason]);
+ Assert.Equal("variant", evaluationEvent.Attributes[TelemetryConstants.Variant]);
+ Assert.Equal("contextId", evaluationEvent.Attributes[TelemetryConstants.ContextId]);
+ Assert.Equal("flagSetId", evaluationEvent.Attributes[TelemetryConstants.FlagSetId]);
+ Assert.Equal("version", evaluationEvent.Attributes[TelemetryConstants.Version]);
+ }
+
+ [Fact]
+ public void Build_ShouldHandleErrorDetails()
+ {
+ // Arrange
+ var clientMetadata = new ClientMetadata("client", "1.0.0");
+ var providerMetadata = new Metadata("provider");
+ var hookContext = new HookContext("flagKey", new Value(), FlagValueType.Object, clientMetadata,
+ providerMetadata, EvaluationContext.Empty);
+ var metadata = new Dictionary
+ {
+ { "flagSetId", "flagSetId" }, { "contextId", "contextId" }, { "version", "version" }
+ };
+ var flagMetadata = new ImmutableMetadata(metadata);
+ var details = new FlagEvaluationDetails("flagKey", new Value("value"), ErrorType.General,
+ errorMessage: "errorMessage", reason: "reason", variant: "variant", flagMetadata: flagMetadata);
+
+ // Act
+ var evaluationEvent = _builder.Build(hookContext, details);
+
+ // Assert
+ Assert.Equal("general", evaluationEvent.Attributes[TelemetryConstants.ErrorCode]);
+ Assert.Equal("errorMessage", evaluationEvent.Attributes[TelemetryConstants.ErrorMessage]);
+ }
+
+ [Fact]
+ public void Build_ShouldHandleMissingVariant()
+ {
+ // Arrange
+ var clientMetadata = new ClientMetadata("client", "1.0.0");
+ var providerMetadata = new Metadata("provider");
+ var hookContext = new HookContext("flagKey", new Value("value"), FlagValueType.Object, clientMetadata,
+ providerMetadata, EvaluationContext.Empty);
+ var metadata = new Dictionary
+ {
+ { "flagSetId", "flagSetId" }, { "contextId", "contextId" }, { "version", "version" }
+ };
+ var flagMetadata = new ImmutableMetadata(metadata);
+ var details = new FlagEvaluationDetails("flagKey", new Value("value"), ErrorType.None,
+ reason: "reason", variant: null, flagMetadata: flagMetadata);
+
+ // Act
+ var evaluationEvent = _builder.Build(hookContext, details);
+
+ // Assert
+ Assert.Null(evaluationEvent.Attributes[TelemetryConstants.Variant]);
+ }
+
+ [Fact]
+ public void Build_ShouldHandleMissingFlagMetadata()
+ {
+ // Arrange
+ var clientMetadata = new ClientMetadata("client", "1.0.0");
+ var providerMetadata = new Metadata("provider");
+ var hookContext = new HookContext("flagKey", new Value("value"), FlagValueType.Object, clientMetadata,
+ providerMetadata, EvaluationContext.Empty);
+ var flagMetadata = new ImmutableMetadata();
+ var details = new FlagEvaluationDetails("flagKey", new Value("value"), ErrorType.None,
+ reason: "reason", variant: "", flagMetadata: flagMetadata);
+
+ // Act
+ var evaluationEvent = _builder.Build(hookContext, details);
+
+ // Assert
+ Assert.Null(evaluationEvent.Attributes[TelemetryConstants.ContextId]);
+ Assert.Null(evaluationEvent.Attributes[TelemetryConstants.FlagSetId]);
+ Assert.Null(evaluationEvent.Attributes[TelemetryConstants.Version]);
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData(" ")]
+ public void Build_ShouldHandleMissingReason(string? reason)
+ {
+ // Arrange
+ var clientMetadata = new ClientMetadata("client", "1.0.0");
+ var providerMetadata = new Metadata("provider");
+ var hookContext = new HookContext("flagKey", new Value("value"), FlagValueType.Object, clientMetadata,
+ providerMetadata, EvaluationContext.Empty);
+ var flagMetadata = new ImmutableMetadata();
+ var details = new FlagEvaluationDetails("flagKey", new Value("value"), ErrorType.None,
+ reason: reason, variant: "", flagMetadata: flagMetadata);
+
+ // Act
+ var evaluationEvent = _builder.Build(hookContext, details);
+
+ // Assert
+ Assert.Equal(Reason.Unknown.ToLowerInvariant(), evaluationEvent.Attributes[TelemetryConstants.Reason]);
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData(" ")]
+ public void Build_ShouldHandleErrorWithEmptyErrorMessage(string? errorMessage)
+ {
+ // Arrange
+ var clientMetadata = new ClientMetadata("client", "1.0.0");
+ var providerMetadata = new Metadata("provider");
+ var hookContext = new HookContext("flagKey", new Value("value"), FlagValueType.Object, clientMetadata,
+ providerMetadata, EvaluationContext.Empty);
+ var flagMetadata = new ImmutableMetadata();
+ var details = new FlagEvaluationDetails("flagKey", new Value("value"), ErrorType.General,
+ errorMessage: errorMessage, reason: "reason", variant: "", flagMetadata: flagMetadata);
+
+ // Act
+ var evaluationEvent = _builder.Build(hookContext, details);
+
+ // Assert
+ Assert.Equal("general", evaluationEvent.Attributes[TelemetryConstants.ErrorCode]);
+ Assert.False(evaluationEvent.Attributes.ContainsKey(TelemetryConstants.ErrorMessage));
+ }
+
+ [Fact]
+ public void Build_ShouldIncludeValueAttributeInEvent()
+ {
+ // Arrange
+ var clientMetadata = new ClientMetadata("client", "1.0.0");
+ var providerMetadata = new Metadata("provider");
+ var hookContext = new HookContext("flagKey", new Value(), FlagValueType.Object, clientMetadata,
+ providerMetadata, EvaluationContext.Empty);
+ var testValue = new Value("test-value");
+ var details = new FlagEvaluationDetails("flagKey", testValue, ErrorType.None,
+ reason: "reason", variant: "variant", flagMetadata: new ImmutableMetadata());
+
+ // Act
+ var evaluationEvent = _builder.Build(hookContext, details);
+
+ // Assert
+ Assert.Equal(testValue, evaluationEvent.Attributes[TelemetryConstants.Value]);
+ }
+}