From 00f75fcd9ec00b39f9c75ba8178dbaa688a12ebe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Thu, 6 Mar 2025 16:37:19 +0000 Subject: [PATCH 01/12] feat: Add TelemetryAttributes for OpenTelemetry compliant event logging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../Telemetry/TelemetryAttributes.cs | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 src/OpenFeature/Telemetry/TelemetryAttributes.cs diff --git a/src/OpenFeature/Telemetry/TelemetryAttributes.cs b/src/OpenFeature/Telemetry/TelemetryAttributes.cs new file mode 100644 index 00000000..fa60cf15 --- /dev/null +++ b/src/OpenFeature/Telemetry/TelemetryAttributes.cs @@ -0,0 +1,53 @@ +namespace OpenFeature.Telemetry; + +/// +/// The attributes of an OpenTelemetry compliant event for flag evaluation. +/// +/// +public static class TelemetryAttributes +{ + /// + /// 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 semantic identifier for an evaluated flag value. + /// + public const string Variant = "feature_flag.variant"; + + /// + /// The unique identifier for the flag evaluation context. For example, the targeting key. + /// + public const string ContextId = "feature_flag.context.id"; + + /// + /// A message explaining the nature of an error occurring during flag evaluation. + /// + public const string ErrorMessage = "feature_flag.evaluation.error.message"; + + /// + /// The reason code which shows how a feature flag value was determined. + /// + public const string Reason = "feature_flag.evaluation.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"; +} From c6287eb54030903a4d3b67f5c302a26f8da7f2ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Thu, 6 Mar 2025 16:38:56 +0000 Subject: [PATCH 02/12] feat: Add TelemetryEvaluationData for feature flag event logging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../Telemetry/TelemetryEvaluationData.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/OpenFeature/Telemetry/TelemetryEvaluationData.cs diff --git a/src/OpenFeature/Telemetry/TelemetryEvaluationData.cs b/src/OpenFeature/Telemetry/TelemetryEvaluationData.cs new file mode 100644 index 00000000..f143dc02 --- /dev/null +++ b/src/OpenFeature/Telemetry/TelemetryEvaluationData.cs @@ -0,0 +1,20 @@ +namespace OpenFeature.Telemetry; + +/** +* Event data, sometimes referred to as "body", is specific to a specific event. +* In this case, the event is `feature_flag.evaluation`. That's why the prefix +* is omitted from the values. +* @see https://opentelemetry.io/docs/specs/semconv/feature-flags/feature-flags-logs/ +*/ +public static class TelemetryEvaluationData +{ + /** + * The evaluated value of the feature flag. + * + * - type: `undefined` + * - requirement level: `conditionally required` + * - condition: variant is not defined on the evaluation details + * - example: `#ff0000`; `1`; `true` + */ + public const string Value = "value"; +} From 38a110a2954431c2d8e6f268a97ff8e6263faa28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Thu, 6 Mar 2025 16:40:32 +0000 Subject: [PATCH 03/12] feat: Add TelemetryFlagMetadata for flag metadata attributes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../Telemetry/TelemetryFlagMetadata.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/OpenFeature/Telemetry/TelemetryFlagMetadata.cs diff --git a/src/OpenFeature/Telemetry/TelemetryFlagMetadata.cs b/src/OpenFeature/Telemetry/TelemetryFlagMetadata.cs new file mode 100644 index 00000000..3bd0a258 --- /dev/null +++ b/src/OpenFeature/Telemetry/TelemetryFlagMetadata.cs @@ -0,0 +1,25 @@ +namespace OpenFeature.Telemetry; + +/** + * Well-known flag metadata attributes for telemetry events. + * @see 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"; +} From 75330fe69e5e28b0140386247ffc8425be97148f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Thu, 6 Mar 2025 17:08:43 +0000 Subject: [PATCH 04/12] feat: Rename TelemetryAttributes to TelemetryConstants and add EvaluationEventFactory for event creation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../Telemetry/EvaluationEventFactory.cs | 57 +++++++++++++++++++ ...tryAttributes.cs => TelemetryConstants.cs} | 2 +- 2 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 src/OpenFeature/Telemetry/EvaluationEventFactory.cs rename src/OpenFeature/Telemetry/{TelemetryAttributes.cs => TelemetryConstants.cs} (97%) diff --git a/src/OpenFeature/Telemetry/EvaluationEventFactory.cs b/src/OpenFeature/Telemetry/EvaluationEventFactory.cs new file mode 100644 index 00000000..01965d12 --- /dev/null +++ b/src/OpenFeature/Telemetry/EvaluationEventFactory.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using OpenFeature.Constant; +using OpenFeature.Model; + +namespace OpenFeature.Telemetry; + +public class EvaluationEvent +{ + public string Name { get; set; } + public Dictionary Attributes { get; set; } + public Dictionary Body { get; set; } +} + +public static class EvaluationEventFactory +{ + + private const string EventName = "feature_flag.evaluation"; + + public static EvaluationEvent CreateEvaluationEvent(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() : "unknown"; + + var body = new Dictionary(); + + if (!string.IsNullOrWhiteSpace(details.Variant)) + { + attributes[TelemetryConstants.Variant] = details.Variant ?? details.Value.ToString(); + } + + attributes[TelemetryFlagMetadata.ContextId] = details.FlagMetadata?.GetString(TelemetryFlagMetadata.ContextId); + attributes[TelemetryFlagMetadata.FlagSetId] = details.FlagMetadata?.GetString(TelemetryFlagMetadata.FlagSetId); + attributes[TelemetryFlagMetadata.Version] = details.FlagMetadata?.GetString(TelemetryFlagMetadata.Version); + + if (details.ErrorType != ErrorType.None) + { + attributes[TelemetryConstants.ErrorCode] = details.ErrorType.ToString(); + + if (!string.IsNullOrWhiteSpace(details.ErrorMessage)) + { + attributes[TelemetryConstants.ErrorMessage] = details.ErrorMessage ?? "N/A"; + } + } + + return new EvaluationEvent + { + Name = EventName, + Attributes = attributes, + Body = body + }; + } +} diff --git a/src/OpenFeature/Telemetry/TelemetryAttributes.cs b/src/OpenFeature/Telemetry/TelemetryConstants.cs similarity index 97% rename from src/OpenFeature/Telemetry/TelemetryAttributes.cs rename to src/OpenFeature/Telemetry/TelemetryConstants.cs index fa60cf15..9cbd3db4 100644 --- a/src/OpenFeature/Telemetry/TelemetryAttributes.cs +++ b/src/OpenFeature/Telemetry/TelemetryConstants.cs @@ -4,7 +4,7 @@ namespace OpenFeature.Telemetry; /// The attributes of an OpenTelemetry compliant event for flag evaluation. /// /// -public static class TelemetryAttributes +public static class TelemetryConstants { /// /// The lookup key of the feature flag. From 783df4f652430bc369bf32a4bbde1547236252d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Thu, 6 Mar 2025 17:12:17 +0000 Subject: [PATCH 05/12] feat: Introduce EvaluationEvent class and refactor EvaluationEventFactory for event creation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- src/OpenFeature/Telemetry/EvaluationEvent.cs | 37 +++++++++++++++++++ .../Telemetry/EvaluationEventFactory.cs | 26 ++++++------- 2 files changed, 48 insertions(+), 15 deletions(-) create mode 100644 src/OpenFeature/Telemetry/EvaluationEvent.cs diff --git a/src/OpenFeature/Telemetry/EvaluationEvent.cs b/src/OpenFeature/Telemetry/EvaluationEvent.cs new file mode 100644 index 00000000..666f7b4c --- /dev/null +++ b/src/OpenFeature/Telemetry/EvaluationEvent.cs @@ -0,0 +1,37 @@ +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. + /// The body of the event. + public EvaluationEvent(string name, Dictionary attributes, Dictionary body) + { + this.Name = name; + this.Attributes = attributes; + this.Body = body; + } + + /// + /// Gets or sets the name of the event. + /// + public string Name { get; set; } + + /// + /// Gets or sets the attributes of the event. + /// + public Dictionary Attributes { get; set; } + + /// + /// Gets or sets the body of the event. + /// + public Dictionary Body { get; set; } +} diff --git a/src/OpenFeature/Telemetry/EvaluationEventFactory.cs b/src/OpenFeature/Telemetry/EvaluationEventFactory.cs index 01965d12..e84928fd 100644 --- a/src/OpenFeature/Telemetry/EvaluationEventFactory.cs +++ b/src/OpenFeature/Telemetry/EvaluationEventFactory.cs @@ -4,18 +4,19 @@ namespace OpenFeature.Telemetry; -public class EvaluationEvent -{ - public string Name { get; set; } - public Dictionary Attributes { get; set; } - public Dictionary Body { get; set; } -} - +/// +/// Factory class for creating evaluation events for feature flags. +/// public static class EvaluationEventFactory { - private const string EventName = "feature_flag.evaluation"; + /// + /// 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 static EvaluationEvent CreateEvaluationEvent(HookContext hookContext, FlagEvaluationDetails details) { var attributes = new Dictionary @@ -24,7 +25,7 @@ public static EvaluationEvent CreateEvaluationEvent(HookContext hookConte { TelemetryConstants.Provider, hookContext.ProviderMetadata.Name } }; - attributes[TelemetryConstants.Reason] = !string.IsNullOrWhiteSpace(details.Reason) ? details.Reason?.ToLowerInvariant() : "unknown"; + attributes[TelemetryConstants.Reason] = !string.IsNullOrWhiteSpace(details.Reason) ? details.Reason?.ToLowerInvariant() : Reason.Unknown; var body = new Dictionary(); @@ -47,11 +48,6 @@ public static EvaluationEvent CreateEvaluationEvent(HookContext hookConte } } - return new EvaluationEvent - { - Name = EventName, - Attributes = attributes, - Body = body - }; + return new EvaluationEvent(EventName, attributes, body); } } From f94d83aa120404d1e0c603b580609c5903f08c0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Fri, 7 Mar 2025 13:38:53 +0000 Subject: [PATCH 06/12] feat: Add EvaluationEventBuilder for creating evaluation events for feature flags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- ...{EvaluationEventFactory.cs => EvaluationEventBuilder.cs} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename src/OpenFeature/Telemetry/{EvaluationEventFactory.cs => EvaluationEventBuilder.cs} (89%) diff --git a/src/OpenFeature/Telemetry/EvaluationEventFactory.cs b/src/OpenFeature/Telemetry/EvaluationEventBuilder.cs similarity index 89% rename from src/OpenFeature/Telemetry/EvaluationEventFactory.cs rename to src/OpenFeature/Telemetry/EvaluationEventBuilder.cs index e84928fd..e982587b 100644 --- a/src/OpenFeature/Telemetry/EvaluationEventFactory.cs +++ b/src/OpenFeature/Telemetry/EvaluationEventBuilder.cs @@ -5,9 +5,9 @@ namespace OpenFeature.Telemetry; /// -/// Factory class for creating evaluation events for feature flags. +/// Class for creating evaluation events for feature flags. /// -public static class EvaluationEventFactory +public static class EvaluationEventBuilder { private const string EventName = "feature_flag.evaluation"; @@ -17,7 +17,7 @@ public static class EvaluationEventFactory /// 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 static EvaluationEvent CreateEvaluationEvent(HookContext hookContext, FlagEvaluationDetails details) + public static EvaluationEvent Build(HookContext hookContext, FlagEvaluationDetails details) { var attributes = new Dictionary { From f6f3d0f53c9943ad5f3fc220cc5a46b5bb852ba0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Fri, 7 Mar 2025 13:57:40 +0000 Subject: [PATCH 07/12] test: Add unit tests for EvaluationEventBuilder to validate event creation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../Telemetry/EvaluationEventBuilderTests.cs | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 test/OpenFeature.Tests/Telemetry/EvaluationEventBuilderTests.cs diff --git a/test/OpenFeature.Tests/Telemetry/EvaluationEventBuilderTests.cs b/test/OpenFeature.Tests/Telemetry/EvaluationEventBuilderTests.cs new file mode 100644 index 00000000..1d65fc2d --- /dev/null +++ b/test/OpenFeature.Tests/Telemetry/EvaluationEventBuilderTests.cs @@ -0,0 +1,88 @@ +using System.Collections.Generic; +using OpenFeature.Constant; +using OpenFeature.Model; +using OpenFeature.Telemetry; +using Xunit; + +namespace OpenFeature.Tests.Telemetry +{ + public class EvaluationEventBuilderTests + { + [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 = EvaluationEventBuilder.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[TelemetryFlagMetadata.ContextId]); + Assert.Equal("flagSetId", evaluationEvent.Attributes[TelemetryFlagMetadata.FlagSetId]); + Assert.Equal("version", evaluationEvent.Attributes[TelemetryFlagMetadata.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 = EvaluationEventBuilder.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: "", flagMetadata: flagMetadata); + + // Act + var evaluationEvent = EvaluationEventBuilder.Build(hookContext, details); + + // Assert + Assert.Throws(() => evaluationEvent.Attributes[TelemetryConstants.Variant]); + } + } +} From 53693481df6ff54b16e0878f6bda14d3e781bf6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Fri, 7 Mar 2025 14:30:32 +0000 Subject: [PATCH 08/12] fix: Simplify variant assignment in EvaluationEventBuilder and enhance tests for missing flag metadata MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../Telemetry/EvaluationEventBuilder.cs | 6 +---- .../Telemetry/EvaluationEventBuilderTests.cs | 23 ++++++++++++++++++- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/OpenFeature/Telemetry/EvaluationEventBuilder.cs b/src/OpenFeature/Telemetry/EvaluationEventBuilder.cs index e982587b..6b67bc65 100644 --- a/src/OpenFeature/Telemetry/EvaluationEventBuilder.cs +++ b/src/OpenFeature/Telemetry/EvaluationEventBuilder.cs @@ -29,11 +29,7 @@ public static EvaluationEvent Build(HookContext hookContext, FlagEvaluati var body = new Dictionary(); - if (!string.IsNullOrWhiteSpace(details.Variant)) - { - attributes[TelemetryConstants.Variant] = details.Variant ?? details.Value.ToString(); - } - + attributes[TelemetryConstants.Variant] = details.Variant; attributes[TelemetryFlagMetadata.ContextId] = details.FlagMetadata?.GetString(TelemetryFlagMetadata.ContextId); attributes[TelemetryFlagMetadata.FlagSetId] = details.FlagMetadata?.GetString(TelemetryFlagMetadata.FlagSetId); attributes[TelemetryFlagMetadata.Version] = details.FlagMetadata?.GetString(TelemetryFlagMetadata.Version); diff --git a/test/OpenFeature.Tests/Telemetry/EvaluationEventBuilderTests.cs b/test/OpenFeature.Tests/Telemetry/EvaluationEventBuilderTests.cs index 1d65fc2d..1965e415 100644 --- a/test/OpenFeature.Tests/Telemetry/EvaluationEventBuilderTests.cs +++ b/test/OpenFeature.Tests/Telemetry/EvaluationEventBuilderTests.cs @@ -75,6 +75,25 @@ public void Build_ShouldHandleMissingVariant() { "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 = EvaluationEventBuilder.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); @@ -82,7 +101,9 @@ public void Build_ShouldHandleMissingVariant() var evaluationEvent = EvaluationEventBuilder.Build(hookContext, details); // Assert - Assert.Throws(() => evaluationEvent.Attributes[TelemetryConstants.Variant]); + Assert.Null(evaluationEvent.Attributes[TelemetryFlagMetadata.ContextId]); + Assert.Null(evaluationEvent.Attributes[TelemetryFlagMetadata.FlagSetId]); + Assert.Null(evaluationEvent.Attributes[TelemetryFlagMetadata.Version]); } } } From 2d81ada7a1b2e5c8058493e923e94ef63c36661a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Fri, 7 Mar 2025 14:32:18 +0000 Subject: [PATCH 09/12] fix: Ensure proper handling of missing reason in EvaluationEventBuilder and add corresponding unit tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../Telemetry/EvaluationEventBuilder.cs | 2 +- .../Telemetry/EvaluationEventBuilderTests.cs | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/OpenFeature/Telemetry/EvaluationEventBuilder.cs b/src/OpenFeature/Telemetry/EvaluationEventBuilder.cs index 6b67bc65..865045d3 100644 --- a/src/OpenFeature/Telemetry/EvaluationEventBuilder.cs +++ b/src/OpenFeature/Telemetry/EvaluationEventBuilder.cs @@ -25,10 +25,10 @@ public static EvaluationEvent Build(HookContext hookContext, FlagEvaluati { TelemetryConstants.Provider, hookContext.ProviderMetadata.Name } }; - attributes[TelemetryConstants.Reason] = !string.IsNullOrWhiteSpace(details.Reason) ? details.Reason?.ToLowerInvariant() : Reason.Unknown; var body = new Dictionary(); + attributes[TelemetryConstants.Reason] = !string.IsNullOrWhiteSpace(details.Reason) ? details.Reason?.ToLowerInvariant() : Reason.Unknown; attributes[TelemetryConstants.Variant] = details.Variant; attributes[TelemetryFlagMetadata.ContextId] = details.FlagMetadata?.GetString(TelemetryFlagMetadata.ContextId); attributes[TelemetryFlagMetadata.FlagSetId] = details.FlagMetadata?.GetString(TelemetryFlagMetadata.FlagSetId); diff --git a/test/OpenFeature.Tests/Telemetry/EvaluationEventBuilderTests.cs b/test/OpenFeature.Tests/Telemetry/EvaluationEventBuilderTests.cs index 1965e415..0d08cad1 100644 --- a/test/OpenFeature.Tests/Telemetry/EvaluationEventBuilderTests.cs +++ b/test/OpenFeature.Tests/Telemetry/EvaluationEventBuilderTests.cs @@ -105,5 +105,27 @@ public void Build_ShouldHandleMissingFlagMetadata() Assert.Null(evaluationEvent.Attributes[TelemetryFlagMetadata.FlagSetId]); Assert.Null(evaluationEvent.Attributes[TelemetryFlagMetadata.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 = EvaluationEventBuilder.Build(hookContext, details); + + // Assert + Assert.Equal(Reason.Unknown, evaluationEvent.Attributes[TelemetryConstants.Reason]); + } } } From 0d1a869424d98202f6f9ac5d76d0e7609e732967 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Tue, 11 Mar 2025 16:26:35 +0000 Subject: [PATCH 10/12] fix: Normalize error code to lowercase in EvaluationEventBuilder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- src/OpenFeature/Telemetry/EvaluationEventBuilder.cs | 2 +- test/OpenFeature.Tests/Telemetry/EvaluationEventBuilderTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/OpenFeature/Telemetry/EvaluationEventBuilder.cs b/src/OpenFeature/Telemetry/EvaluationEventBuilder.cs index 865045d3..4dc17aab 100644 --- a/src/OpenFeature/Telemetry/EvaluationEventBuilder.cs +++ b/src/OpenFeature/Telemetry/EvaluationEventBuilder.cs @@ -36,7 +36,7 @@ public static EvaluationEvent Build(HookContext hookContext, FlagEvaluati if (details.ErrorType != ErrorType.None) { - attributes[TelemetryConstants.ErrorCode] = details.ErrorType.ToString(); + attributes[TelemetryConstants.ErrorCode] = details.ErrorType.ToString()?.ToLowerInvariant(); if (!string.IsNullOrWhiteSpace(details.ErrorMessage)) { diff --git a/test/OpenFeature.Tests/Telemetry/EvaluationEventBuilderTests.cs b/test/OpenFeature.Tests/Telemetry/EvaluationEventBuilderTests.cs index 0d08cad1..2c56c360 100644 --- a/test/OpenFeature.Tests/Telemetry/EvaluationEventBuilderTests.cs +++ b/test/OpenFeature.Tests/Telemetry/EvaluationEventBuilderTests.cs @@ -58,7 +58,7 @@ public void Build_ShouldHandleErrorDetails() var evaluationEvent = EvaluationEventBuilder.Build(hookContext, details); // Assert - Assert.Equal("General", evaluationEvent.Attributes[TelemetryConstants.ErrorCode]); + Assert.Equal("general", evaluationEvent.Attributes[TelemetryConstants.ErrorCode]); Assert.Equal("errorMessage", evaluationEvent.Attributes[TelemetryConstants.ErrorMessage]); } From 26d7dd5930c5d719bad250387240fbb44b9b8d71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Thu, 17 Apr 2025 16:45:13 +0100 Subject: [PATCH 11/12] fix: Correct telemetry constant values for feature flag attributes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- src/OpenFeature/Telemetry/TelemetryConstants.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/OpenFeature/Telemetry/TelemetryConstants.cs b/src/OpenFeature/Telemetry/TelemetryConstants.cs index 9cbd3db4..3660c82d 100644 --- a/src/OpenFeature/Telemetry/TelemetryConstants.cs +++ b/src/OpenFeature/Telemetry/TelemetryConstants.cs @@ -19,7 +19,7 @@ public static class TelemetryConstants /// /// A semantic identifier for an evaluated flag value. /// - public const string Variant = "feature_flag.variant"; + public const string Variant = "feature_flag.result.variant"; /// /// The unique identifier for the flag evaluation context. For example, the targeting key. @@ -29,17 +29,17 @@ public static class TelemetryConstants /// /// A message explaining the nature of an error occurring during flag evaluation. /// - public const string ErrorMessage = "feature_flag.evaluation.error.message"; + public const string ErrorMessage = "error.message"; /// /// The reason code which shows how a feature flag value was determined. /// - public const string Reason = "feature_flag.evaluation.reason"; + 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"; + public const string Provider = "feature_flag.provider.name"; /// /// The identifier of the flag set to which the feature flag belongs. From 6b484877a4807f7e6363f47e38f6caf69b53045f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Mon, 28 Apr 2025 16:34:53 +0100 Subject: [PATCH 12/12] refactor(tests): streamline EvaluationEventBuilderTests for clarity and consistency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../Telemetry/EvaluationEventBuilderTests.cs | 235 +++++++++--------- 1 file changed, 117 insertions(+), 118 deletions(-) diff --git a/test/OpenFeature.Tests/Telemetry/EvaluationEventBuilderTests.cs b/test/OpenFeature.Tests/Telemetry/EvaluationEventBuilderTests.cs index 2c56c360..99805f2b 100644 --- a/test/OpenFeature.Tests/Telemetry/EvaluationEventBuilderTests.cs +++ b/test/OpenFeature.Tests/Telemetry/EvaluationEventBuilderTests.cs @@ -4,128 +4,127 @@ using OpenFeature.Telemetry; using Xunit; -namespace OpenFeature.Tests.Telemetry +namespace OpenFeature.Tests.Telemetry; + +public class EvaluationEventBuilderTests { - public class EvaluationEventBuilderTests + [Fact] + public void Build_ShouldReturnEventWithCorrectAttributes() { - [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 = EvaluationEventBuilder.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[TelemetryFlagMetadata.ContextId]); - Assert.Equal("flagSetId", evaluationEvent.Attributes[TelemetryFlagMetadata.FlagSetId]); - Assert.Equal("version", evaluationEvent.Attributes[TelemetryFlagMetadata.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 { - // 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 = EvaluationEventBuilder.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 = EvaluationEventBuilder.Build(hookContext, details); - - // Assert - Assert.Null(evaluationEvent.Attributes[TelemetryConstants.Variant]); - } - - [Fact] - public void Build_ShouldHandleMissingFlagMetadata() + { "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 = EvaluationEventBuilder.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[TelemetryFlagMetadata.ContextId]); + Assert.Equal("flagSetId", evaluationEvent.Attributes[TelemetryFlagMetadata.FlagSetId]); + Assert.Equal("version", evaluationEvent.Attributes[TelemetryFlagMetadata.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 { - // 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 = EvaluationEventBuilder.Build(hookContext, details); - - // Assert - Assert.Null(evaluationEvent.Attributes[TelemetryFlagMetadata.ContextId]); - Assert.Null(evaluationEvent.Attributes[TelemetryFlagMetadata.FlagSetId]); - Assert.Null(evaluationEvent.Attributes[TelemetryFlagMetadata.Version]); - } - - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData(" ")] - public void Build_ShouldHandleMissingReason(string? reason) + { "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 = EvaluationEventBuilder.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 { - // 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 = EvaluationEventBuilder.Build(hookContext, details); - - // Assert - Assert.Equal(Reason.Unknown, evaluationEvent.Attributes[TelemetryConstants.Reason]); - } + { "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 = EvaluationEventBuilder.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 = EvaluationEventBuilder.Build(hookContext, details); + + // Assert + Assert.Null(evaluationEvent.Attributes[TelemetryFlagMetadata.ContextId]); + Assert.Null(evaluationEvent.Attributes[TelemetryFlagMetadata.FlagSetId]); + Assert.Null(evaluationEvent.Attributes[TelemetryFlagMetadata.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 = EvaluationEventBuilder.Build(hookContext, details); + + // Assert + Assert.Equal(Reason.Unknown, evaluationEvent.Attributes[TelemetryConstants.Reason]); } }