11package datadog .trace .api .openfeature ;
22
3+ import static org .junit .jupiter .api .Assertions .assertEquals ;
34import static org .mockito .ArgumentMatchers .eq ;
45import static org .mockito .Mockito .mock ;
56import static org .mockito .Mockito .verify ;
89import dev .openfeature .sdk .ErrorCode ;
910import io .opentelemetry .api .common .Attributes ;
1011import io .opentelemetry .api .metrics .LongCounter ;
12+ import io .opentelemetry .exporter .otlp .http .metrics .OtlpHttpMetricExporter ;
13+ import io .opentelemetry .sdk .metrics .InstrumentType ;
14+ import io .opentelemetry .sdk .metrics .SdkMeterProvider ;
15+ import io .opentelemetry .sdk .metrics .data .AggregationTemporality ;
16+ import io .opentelemetry .sdk .metrics .data .LongPointData ;
17+ import io .opentelemetry .sdk .metrics .data .MetricData ;
18+ import io .opentelemetry .sdk .metrics .export .AggregationTemporalitySelector ;
19+ import io .opentelemetry .sdk .testing .exporter .InMemoryMetricReader ;
20+ import java .util .Collection ;
1121import org .junit .jupiter .api .Test ;
1222import org .mockito .ArgumentCaptor ;
1323
@@ -121,7 +131,7 @@ void recordNullReasonBecomesUnknown() {
121131
122132 @ Test
123133 void recordIsNoOpWhenCounterIsNull () {
124- FlagEvalMetrics metrics = new FlagEvalMetrics (null );
134+ FlagEvalMetrics metrics = new FlagEvalMetrics (( LongCounter ) null );
125135 // Should not throw
126136 metrics .record ("my-flag" , "on" , "TARGETING_MATCH" , null , null );
127137 }
@@ -137,6 +147,51 @@ void shutdownClearsCounter() {
137147 verifyNoInteractions (counter );
138148 }
139149
150+ @ Test
151+ void exporterIsConfiguredWithCumulativeTemporalityForCounters () {
152+ // Regression guard: FlagEvalMetrics must explicitly configure alwaysCumulative() so that
153+ // the Datadog agent receives absolute counts rather than delta values that may be converted
154+ // to rates. This test documents and enforces that the exporter uses CUMULATIVE for counters.
155+ try (OtlpHttpMetricExporter exporter =
156+ OtlpHttpMetricExporter .builder ()
157+ .setAggregationTemporalitySelector (AggregationTemporalitySelector .alwaysCumulative ())
158+ .build ()) {
159+ assertEquals (
160+ AggregationTemporality .CUMULATIVE ,
161+ exporter .getAggregationTemporality (InstrumentType .COUNTER ),
162+ "alwaysCumulative() selector must produce CUMULATIVE for counters" );
163+ }
164+ }
165+
166+ @ Test
167+ void multipleRecordCallsAccumulateCumulativelyInExportedMetrics () {
168+ // Use InMemoryMetricReader with cumulative temporality (matching what FlagEvalMetrics
169+ // configures on the OTLP exporter) to verify that N record() calls produce a sum of N.
170+ InMemoryMetricReader reader = InMemoryMetricReader .create ();
171+ SdkMeterProvider provider = SdkMeterProvider .builder ().registerMetricReader (reader ).build ();
172+
173+ try (FlagEvalMetrics metrics = new FlagEvalMetrics (provider )) {
174+ for (int i = 0 ; i < 5 ; i ++) {
175+ metrics .record ("count-flag" , "on" , "STATIC" , null , "default-alloc" );
176+ }
177+
178+ Collection <MetricData > data = reader .collectAllMetrics ();
179+ MetricData metric =
180+ data .stream ()
181+ .filter (m -> m .getName ().equals ("feature_flag.evaluations" ))
182+ .findFirst ()
183+ .orElseThrow (() -> new AssertionError ("feature_flag.evaluations metric not found" ));
184+
185+ assertEquals (
186+ AggregationTemporality .CUMULATIVE ,
187+ metric .getLongSumData ().getAggregationTemporality (),
188+ "Exported metric must use CUMULATIVE temporality" );
189+
190+ LongPointData point = metric .getLongSumData ().getPoints ().iterator ().next ();
191+ assertEquals (5L , point .getValue (), "5 record() calls must produce a cumulative sum of 5" );
192+ }
193+ }
194+
140195 private static void assertAttribute (Attributes attrs , String key , String expected ) {
141196 String value =
142197 attrs .asMap ().entrySet ().stream ()
0 commit comments