• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

felleslosninger / einnsyn-backend / 23748274593

30 Mar 2026 01:49PM UTC coverage: 85.109% (-0.04%) from 85.152%
23748274593

Pull #645

github

web-flow
Merge 646323a7f into 146e69633
Pull Request #645: EIN-4841: Implement indexable DownloadCount entity

2686 of 3624 branches covered (74.12%)

Branch coverage included in aggregate %.

8036 of 8974 relevant lines covered (89.55%)

3.74 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.17
src/main/java/no/einnsyn/backend/common/statistics/StatisticsService.java
1
package no.einnsyn.backend.common.statistics;
2

3
import co.elastic.clients.elasticsearch.ElasticsearchClient;
4
import co.elastic.clients.elasticsearch._types.aggregations.Aggregate;
5
import co.elastic.clients.elasticsearch._types.aggregations.Aggregation;
6
import co.elastic.clients.elasticsearch._types.aggregations.CalendarInterval;
7
import co.elastic.clients.elasticsearch._types.query_dsl.BoolQuery;
8
import co.elastic.clients.elasticsearch._types.query_dsl.RangeQuery;
9
import co.elastic.clients.elasticsearch._types.query_dsl.TermQuery;
10
import co.elastic.clients.elasticsearch.core.SearchRequest;
11
import co.elastic.clients.elasticsearch.core.SearchResponse;
12
import java.io.IOException;
13
import java.time.LocalDate;
14
import java.time.LocalDateTime;
15
import java.time.temporal.ChronoUnit;
16
import java.util.ArrayList;
17
import java.util.LinkedHashMap;
18
import java.util.Map;
19
import java.util.function.ObjIntConsumer;
20
import lombok.extern.slf4j.Slf4j;
21
import no.einnsyn.backend.common.exceptions.models.EInnsynException;
22
import no.einnsyn.backend.common.exceptions.models.InternalServerErrorException;
23
import no.einnsyn.backend.common.search.SearchQueryService;
24
import no.einnsyn.backend.common.statistics.models.StatisticsParameters;
25
import no.einnsyn.backend.common.statistics.models.StatisticsResponse;
26
import org.springframework.beans.factory.annotation.Value;
27
import org.springframework.stereotype.Service;
28
import org.springframework.util.StringUtils;
29

30
@Slf4j
4✔
31
@Service
32
@SuppressWarnings("java:S1192") // Allow string literals
33
public class StatisticsService {
34

35
  private static final int MAX_BUCKETS = 1000;
36
  private static final String INTERVAL_HOUR = "hour";
37
  private static final String INTERVAL_DAY = "day";
38
  private static final String INTERVAL_WEEK = "week";
39

40
  private final ElasticsearchClient esClient;
41
  private final SearchQueryService searchQueryService;
42

43
  @Value("${application.elasticsearch.index}")
44
  private String elasticsearchIndex;
45

46
  public StatisticsService(ElasticsearchClient esClient, SearchQueryService searchQueryService) {
2✔
47
    this.esClient = esClient;
3✔
48
    this.searchQueryService = searchQueryService;
3✔
49
  }
1✔
50

51
  /**
52
   * Query statistics based on the provided parameters
53
   *
54
   * @param statisticsParameters the parameters for filtering and aggregating statistics
55
   * @return the statistics response containing summary, time series, and metadata
56
   * @throws EInnsynException if the query fails
57
   */
58
  public StatisticsResponse query(StatisticsParameters statisticsParameters)
59
      throws EInnsynException {
60
    var queryBuilder = searchQueryService.getQueryBuilder(statisticsParameters);
5✔
61

62
    // Get aggregate from/to range. Incoming values has already been validated when parsed.
63
    var aggregateTo =
64
        statisticsParameters.getAggregateTo() != null
3✔
65
            ? LocalDate.parse(statisticsParameters.getAggregateTo()).plusDays(1).atStartOfDay()
7✔
66
            : LocalDate.now().plusDays(1).atStartOfDay();
5✔
67
    var aggregateFrom =
68
        statisticsParameters.getAggregateFrom() != null
3✔
69
            ? LocalDate.parse(statisticsParameters.getAggregateFrom()).atStartOfDay()
5✔
70
            : aggregateTo.minusYears(1);
4✔
71
    var calendarInterval =
4✔
72
        calculateCalendarInterval(
2✔
73
            aggregateFrom, aggregateTo, statisticsParameters.getAggregateInterval());
1✔
74

75
    // No need to check documents created after the aggregation range
76
    if (aggregateTo.isBefore(LocalDateTime.now())) {
4✔
77
      var createdDateRangeQuery = getCreatedDateRangeQuery(null, aggregateTo);
5✔
78
      if (createdDateRangeQuery != null) {
2!
79
        queryBuilder.filter(f -> f.range(createdDateRangeQuery));
9✔
80
      }
81
    }
82

83
    var query = queryBuilder.build();
3✔
84
    var createdCountAggregation =
4✔
85
        buildCreatedCountAggregation(aggregateFrom, aggregateTo, calendarInterval);
2✔
86
    var innsynskravCountAggregation =
4✔
87
        buildInnsynskravCountAggregation(aggregateFrom, aggregateTo, calendarInterval);
2✔
88
    var downloadCountAggregation =
4✔
89
        buildDownloadCountAggregation(aggregateFrom, aggregateTo, calendarInterval);
2✔
90
    var fulltextCountAggregation =
4✔
91
        buildFulltextCountAggregation(aggregateFrom, aggregateTo, calendarInterval);
2✔
92

93
    var searchRequestBuilder = new SearchRequest.Builder();
4✔
94
    searchRequestBuilder.index(elasticsearchIndex);
7✔
95
    searchRequestBuilder.query(q -> q.bool(query));
9✔
96
    searchRequestBuilder.size(0); // Don't fetch results
5✔
97
    searchRequestBuilder.aggregations("innsynskravCount", innsynskravCountAggregation);
5✔
98
    searchRequestBuilder.aggregations("downloadCount", downloadCountAggregation);
5✔
99
    searchRequestBuilder.aggregations("fulltextCount", fulltextCountAggregation);
5✔
100
    searchRequestBuilder.aggregations("createdCount", createdCountAggregation);
5✔
101
    var searchRequest = searchRequestBuilder.build();
3✔
102

103
    try {
104
      log.debug("getStatistics() request: {}", searchRequest.toString());
5✔
105
      var searchResponse = esClient.search(searchRequest, Void.class);
6✔
106
      log.debug("getStatistics() response: {}", searchResponse.toString());
5✔
107
      return buildResponse(searchResponse, aggregateFrom, aggregateTo, calendarInterval);
7✔
108
    } catch (IOException e) {
×
109
      throw new InternalServerErrorException("Failed to get statistics", e);
×
110
    }
111
  }
112

113
  /**
114
   * Build statistics response from Elasticsearch search response
115
   *
116
   * @param response the Elasticsearch search response containing aggregations
117
   * @param aggregateFrom the start date of the aggregation
118
   * @param aggregateTo the end date of the aggregation
119
   * @param calendarInterval the interval used for the aggregation
120
   * @return the statistics response with populated summary, time series, and metadata
121
   */
122
  @SuppressWarnings("java:S1192") // Allow string literals
123
  StatisticsResponse buildResponse(
124
      SearchResponse<Void> response,
125
      LocalDateTime aggregateFrom,
126
      LocalDateTime aggregateTo,
127
      CalendarInterval calendarInterval) {
128
    var innsynskravAggregations = response.aggregations().get("innsynskravCount");
6✔
129
    var downloadAggregations = response.aggregations().get("downloadCount");
6✔
130
    var fulltextCountAggregations = response.aggregations().get("fulltextCount");
6✔
131
    var createdCountAggregations = response.aggregations().get("createdCount");
6✔
132
    var statisticsResponse = new StatisticsResponse();
4✔
133

134
    // Build summary
135
    var summary = new StatisticsResponse.Summary();
4✔
136
    statisticsResponse.setSummary(summary);
3✔
137

138
    // Set total document count
139
    if (createdCountAggregations != null && createdCountAggregations.isFilter()) {
5!
140
      summary.setCreatedCount((int) createdCountAggregations.filter().docCount());
8✔
141
    } else {
142
      summary.setCreatedCount(0);
×
143
    }
144

145
    // Set total fulltext count
146
    if (fulltextCountAggregations != null && fulltextCountAggregations.isFilter()) {
5!
147
      summary.setCreatedWithFulltextCount((int) fulltextCountAggregations.filter().docCount());
8✔
148
    } else {
149
      summary.setCreatedWithFulltextCount(0);
×
150
    }
151

152
    // Set total innsynskrav children count
153
    if (innsynskravAggregations != null && innsynskravAggregations.isChildren()) {
5!
154
      var filteredAgg = innsynskravAggregations.children().aggregations().get("filtered");
7✔
155
      if (filteredAgg != null && filteredAgg.isFilter()) {
5!
156
        summary.setCreatedInnsynskravCount((int) filteredAgg.filter().docCount());
7✔
157
      }
158
    }
159
    if (summary.getCreatedInnsynskravCount() == null) {
3!
160
      summary.setCreatedInnsynskravCount(0);
×
161
    }
162

163
    if (downloadAggregations != null && downloadAggregations.isChildren()) {
5!
164
      var filteredAgg = downloadAggregations.children().aggregations().get("filtered");
7✔
165
      summary.setDownloadCount(extractSumAggregationValue(filteredAgg, "countSum"));
6✔
166
    }
167
    if (summary.getDownloadCount() == null) {
3!
168
      summary.setDownloadCount(0);
×
169
    }
170

171
    // Build timeSeries - collect all unique time buckets from all aggregations
172
    var timeSeriesMap = new LinkedHashMap<String, StatisticsResponse.TimeSeries>();
4✔
173

174
    // Collect buckets from all aggregations
175
    collectTimeSeriesBuckets(
5✔
176
        createdCountAggregations, timeSeriesMap, StatisticsResponse.TimeSeries::setCreatedCount);
177
    collectTimeSeriesBuckets(
5✔
178
        fulltextCountAggregations,
179
        timeSeriesMap,
180
        StatisticsResponse.TimeSeries::setCreatedWithFulltextCount);
181

182
    // Handle innsynskrav aggregation (needs to extract filtered child aggregation first)
183
    if (innsynskravAggregations != null && innsynskravAggregations.isChildren()) {
5!
184
      var filteredAgg = innsynskravAggregations.children().aggregations().get("filtered");
7✔
185
      collectTimeSeriesBuckets(
5✔
186
          filteredAgg, timeSeriesMap, StatisticsResponse.TimeSeries::setCreatedInnsynskravCount);
187
    }
188

189
    if (downloadAggregations != null && downloadAggregations.isChildren()) {
5!
190
      var filteredAgg = downloadAggregations.children().aggregations().get("filtered");
7✔
191
      collectTimeSeriesSumBuckets(
6✔
192
          filteredAgg, timeSeriesMap, "countSum", StatisticsResponse.TimeSeries::setDownloadCount);
193
    }
194

195
    // Convert map to list maintaining insertion order
196
    var timeSeries = new ArrayList<>(timeSeriesMap.values());
6✔
197
    statisticsResponse.setTimeSeries(timeSeries);
3✔
198

199
    // Build metadata
200
    var metadata = new StatisticsResponse.Metadata();
4✔
201
    statisticsResponse.setMetadata(metadata);
3✔
202
    metadata.setAggregateFrom(aggregateFrom.toLocalDate().toString());
5✔
203
    // Internally we treat aggregateTo as an exclusive upper bound (start of the day after the
204
    // requested end date). The API response metadata should reflect the user-facing end date.
205
    metadata.setAggregateTo(aggregateTo.minusDays(1).toLocalDate().toString());
7✔
206
    metadata.setAggregateInterval(calendarInterval.jsonValue());
4✔
207

208
    return statisticsResponse;
2✔
209
  }
210

211
  /**
212
   * Build aggregation for counting verified innsynskrav children over time
213
   *
214
   * @param aggregateFrom the start date for filtering
215
   * @param aggregateTo the end date for filtering
216
   * @param calendarInterval the interval for the date histogram
217
   * @return the children aggregation with date histogram buckets
218
   */
219
  Aggregation buildInnsynskravCountAggregation(
220
      LocalDateTime aggregateFrom, LocalDateTime aggregateTo, CalendarInterval calendarInterval) {
221

222
    // Filter children by verified, and created range
223
    var childrenFilterQueryBuilder = new BoolQuery.Builder();
4✔
224
    var aggregationDateRangeQuery = getCreatedDateRangeQuery(aggregateFrom, aggregateTo);
5✔
225
    if (aggregationDateRangeQuery != null) {
2!
226
      childrenFilterQueryBuilder.filter(f -> f.range(aggregationDateRangeQuery));
9✔
227
    }
228

229
    // Filter by verified
230
    var verifiedTermQuery = TermQuery.of(t -> t.field("verified").value(v -> v.booleanValue(true)));
13✔
231
    childrenFilterQueryBuilder.filter(f -> f.term(verifiedTermQuery));
9✔
232

233
    var histogramAgg =
2✔
234
        Aggregation.of(
2✔
235
            a ->
236
                a.dateHistogram(
5✔
237
                    h -> h.field("created").calendarInterval(calendarInterval).minDocCount(1)));
9✔
238

239
    return Aggregation.of(
5✔
240
        a ->
241
            a.children(c -> c.type("innsynskrav"))
12✔
242
                .aggregations(
1✔
243
                    "filtered",
244
                    Aggregation.of(
1✔
245
                        f ->
246
                            f.filter(q -> q.bool(childrenFilterQueryBuilder.build()))
12✔
247
                                .aggregations("buckets", histogramAgg))));
1✔
248
  }
249

250
  /**
251
   * Build aggregation for counting document downloads over time. Download documents are stored as
252
   * hourly child buckets with a numeric count field, so this uses a sum aggregation instead of
253
   * child doc_count.
254
   */
255
  Aggregation buildDownloadCountAggregation(
256
      LocalDateTime aggregateFrom, LocalDateTime aggregateTo, CalendarInterval calendarInterval) {
257

258
    var childrenFilterQueryBuilder = new BoolQuery.Builder();
4✔
259
    var aggregationDateRangeQuery = getCreatedDateRangeQuery(aggregateFrom, aggregateTo);
5✔
260
    if (aggregationDateRangeQuery != null) {
2!
261
      childrenFilterQueryBuilder.filter(f -> f.range(aggregationDateRangeQuery));
9✔
262
    }
263

264
    // "count" field comes from DownloadCountES.count in the download child documents
265
    var sumAgg = Aggregation.of(a -> a.sum(s -> s.field("count")));
12✔
266
    var histogramAgg =
3✔
267
        Aggregation.of(
2✔
268
            a ->
269
                a.dateHistogram(
7✔
270
                        h -> h.field("created").calendarInterval(calendarInterval).minDocCount(1))
9✔
271
                    .aggregations("countSum", sumAgg));
1✔
272

273
    return Aggregation.of(
6✔
274
        a ->
275
            a.children(c -> c.type("download"))
13✔
276
                .aggregations(
1✔
277
                    "filtered",
278
                    Aggregation.of(
1✔
279
                        f ->
280
                            f.filter(q -> q.bool(childrenFilterQueryBuilder.build()))
12✔
281
                                .aggregations("countSum", sumAgg)
3✔
282
                                .aggregations("buckets", histogramAgg))));
1✔
283
  }
284

285
  /**
286
   * Build aggregation for counting all documents over time
287
   *
288
   * @param aggregateFrom the start date for filtering
289
   * @param aggregateTo the end date for filtering
290
   * @param calendarInterval the interval for the date histogram
291
   * @return the filter aggregation with date histogram buckets for all documents
292
   */
293
  Aggregation buildCreatedCountAggregation(
294
      LocalDateTime aggregateFrom, LocalDateTime aggregateTo, CalendarInterval calendarInterval) {
295

296
    // Filter by created date range
297
    var aggregationDateRangeQuery = getCreatedDateRangeQuery(aggregateFrom, aggregateTo);
5✔
298
    var filterQueryBuilder = new BoolQuery.Builder();
4✔
299
    filterQueryBuilder.filter(f -> f.range(aggregationDateRangeQuery));
9✔
300

301
    var histogramAgg =
2✔
302
        Aggregation.of(
2✔
303
            a ->
304
                a.dateHistogram(
5✔
305
                    h -> h.field("created").calendarInterval(calendarInterval).minDocCount(1)));
9✔
306

307
    return Aggregation.of(
5✔
308
        f ->
309
            f.filter(q -> q.bool(filterQueryBuilder.build()))
12✔
310
                .aggregations("buckets", histogramAgg));
1✔
311
  }
312

313
  /**
314
   * Build aggregation for documents with fulltext property
315
   *
316
   * @param aggregateFrom the start date for filtering
317
   * @param aggregateTo the end date for filtering
318
   * @param calendarInterval the interval for the date histogram
319
   * @return the filter aggregation with date histogram buckets for documents with fulltext=true
320
   */
321
  Aggregation buildFulltextCountAggregation(
322
      LocalDateTime aggregateFrom, LocalDateTime aggregateTo, CalendarInterval calendarInterval) {
323

324
    // Filter by created date range
325
    var aggregationDateRangeQuery = getCreatedDateRangeQuery(aggregateFrom, aggregateTo);
5✔
326
    var filterQueryBuilder = new BoolQuery.Builder();
4✔
327
    filterQueryBuilder.filter(f -> f.range(aggregationDateRangeQuery));
9✔
328

329
    // Filter by fulltext = true
330
    var fulltextTermQuery = TermQuery.of(t -> t.field("fulltext").value(v -> v.booleanValue(true)));
13✔
331
    filterQueryBuilder.filter(f -> f.term(fulltextTermQuery));
9✔
332

333
    var histogramAgg =
2✔
334
        Aggregation.of(
2✔
335
            a ->
336
                a.dateHistogram(
5✔
337
                    h -> h.field("created").calendarInterval(calendarInterval).minDocCount(1)));
9✔
338

339
    return Aggregation.of(
5✔
340
        f ->
341
            f.filter(q -> q.bool(filterQueryBuilder.build()))
12✔
342
                .aggregations("buckets", histogramAgg));
1✔
343
  }
344

345
  /**
346
   * Select a date histogram bucket interval for the given date range.
347
   *
348
   * <p>The returned interval is the most fine-grained interval that does not exceed {@link
349
   * #MAX_BUCKETS}. If {@code requestedInterval} is provided, it is treated as the <em>maximum</em>
350
   * desired resolution (hour/day/week/month). If that would exceed the bucket limit, the method
351
   * falls back to progressively coarser intervals until it fits.
352
   *
353
   * <p>If {@code requestedInterval} is {@code null} / blank / unrecognized, the method defaults to
354
   * trying {@code hour} first.
355
   *
356
   * @param aggregateFrom the start date in ISO-8601 format (yyyy-MM-dd)
357
   * @param aggregateTo the end date in ISO-8601 format (yyyy-MM-dd)
358
   * @param requestedInterval the desired maximum resolution: hour/day/week/month (case-insensitive)
359
   * @return the chosen bucket interval (Hour/Day/Week/Month/Year)
360
   */
361
  private CalendarInterval calculateCalendarInterval(
362
      LocalDateTime aggregateFrom, LocalDateTime aggregateTo, String requestedInterval) {
363

364
    var aggregateFromDate = aggregateFrom.toLocalDate().atStartOfDay();
4✔
365
    var aggregateToDate = aggregateTo.toLocalDate().atStartOfDay();
4✔
366

367
    if (!StringUtils.hasText(requestedInterval)) {
3!
368
      requestedInterval = INTERVAL_HOUR;
×
369
    } else {
370
      requestedInterval = requestedInterval.toLowerCase();
3✔
371
    }
372

373
    if (INTERVAL_HOUR.equals(requestedInterval)) {
4✔
374
      var hours = ChronoUnit.HOURS.between(aggregateFromDate, aggregateToDate) + 24;
7✔
375
      if (hours <= MAX_BUCKETS) {
4✔
376
        return CalendarInterval.Hour;
2✔
377
      }
378
      // Resolution too high, fall back to day
379
      requestedInterval = INTERVAL_DAY;
2✔
380
    }
381

382
    if (INTERVAL_DAY.equals(requestedInterval)) {
4✔
383
      var days = ChronoUnit.DAYS.between(aggregateFromDate, aggregateToDate) + 1;
7✔
384
      if (days <= MAX_BUCKETS) {
4✔
385
        return CalendarInterval.Day;
2✔
386
      }
387
      // Resolution too high, fall back to week
388
      requestedInterval = INTERVAL_WEEK;
2✔
389
    }
390

391
    if (INTERVAL_WEEK.equals(requestedInterval)) {
4✔
392
      var weeks = ChronoUnit.WEEKS.between(aggregateFromDate, aggregateToDate) + 1;
7✔
393
      if (weeks <= MAX_BUCKETS) {
4✔
394
        return CalendarInterval.Week;
2✔
395
      }
396
    }
397

398
    return CalendarInterval.Month;
2✔
399
  }
400

401
  /**
402
   * Create a range query for filtering documents by created date
403
   *
404
   * @param aggregateFrom the start date (inclusive), or null for no lower bound
405
   * @param aggregateTo the end date (inclusive), or null for no upper bound
406
   * @return the range query, or null if both parameters are empty
407
   */
408
  RangeQuery getCreatedDateRangeQuery(LocalDateTime aggregateFrom, LocalDateTime aggregateTo) {
409
    if (aggregateFrom == null && aggregateTo == null) {
4!
410
      return null;
×
411
    }
412

413
    return RangeQuery.of(
5✔
414
        r ->
415
            r.date(
6✔
416
                d -> {
417
                  if (aggregateFrom != null) {
2✔
418
                    d.field("created").gte(aggregateFrom.toString());
7✔
419
                  }
420
                  if (aggregateTo != null) {
2!
421
                    d.field("created").lte(aggregateTo.toString());
7✔
422
                  }
423
                  return d;
2✔
424
                }));
425
  }
426

427
  /**
428
   * Helper method to collect time series buckets from aggregation results into a map
429
   *
430
   * @param aggregation the aggregation result to extract buckets from
431
   * @param timeSeriesMap the map to populate with time series data points
432
   * @param setter the consumer to set the count on each data point
433
   */
434
  private void collectTimeSeriesBuckets(
435
      Aggregate aggregation,
436
      Map<String, StatisticsResponse.TimeSeries> timeSeriesMap,
437
      ObjIntConsumer<StatisticsResponse.TimeSeries> setter) {
438
    if (aggregation != null && aggregation.isFilter()) {
5!
439
      var buckets = aggregation.filter().aggregations().get("buckets");
7✔
440
      if (buckets != null && buckets.isDateHistogram()) {
5!
441
        var dateHistogram = buckets.dateHistogram();
3✔
442
        for (var bucket : dateHistogram.buckets().array()) {
12✔
443
          var time = bucket.keyAsString();
3✔
444
          var dataPoint =
3✔
445
              timeSeriesMap.computeIfAbsent(
3✔
446
                  time,
447
                  k -> {
448
                    var point = new StatisticsResponse.TimeSeries();
4✔
449
                    point.setTime(k);
3✔
450
                    point.setCreatedCount(0);
4✔
451
                    point.setCreatedWithFulltextCount(0);
4✔
452
                    point.setCreatedInnsynskravCount(0);
4✔
453
                    point.setDownloadCount(0);
4✔
454
                    return point;
2✔
455
                  });
456
          setter.accept(dataPoint, (int) bucket.docCount());
6✔
457
        }
1✔
458
      }
459
    }
460
  }
1✔
461

462
  private void collectTimeSeriesSumBuckets(
463
      Aggregate aggregation,
464
      Map<String, StatisticsResponse.TimeSeries> timeSeriesMap,
465
      String sumAggregationName,
466
      ObjIntConsumer<StatisticsResponse.TimeSeries> setter) {
467
    if (aggregation != null && aggregation.isFilter()) {
5!
468
      var buckets = aggregation.filter().aggregations().get("buckets");
7✔
469
      if (buckets != null && buckets.isDateHistogram()) {
5!
470
        var dateHistogram = buckets.dateHistogram();
3✔
471
        for (var bucket : dateHistogram.buckets().array()) {
12✔
472
          var time = bucket.keyAsString();
3✔
473
          var dataPoint =
3✔
474
              timeSeriesMap.computeIfAbsent(
3✔
475
                  time,
476
                  k -> {
477
                    var point = new StatisticsResponse.TimeSeries();
×
478
                    point.setTime(k);
×
479
                    point.setCreatedCount(0);
×
480
                    point.setCreatedWithFulltextCount(0);
×
481
                    point.setCreatedInnsynskravCount(0);
×
482
                    point.setDownloadCount(0);
×
483
                    return point;
×
484
                  });
485

486
          var metric = bucket.aggregations().get(sumAggregationName);
6✔
487
          var value = metric != null && metric.isSum() ? metric.sum().value() : 0d;
11!
488
          setter.accept(dataPoint, (int) Math.round(value));
6✔
489
        }
1✔
490
      }
491
    }
492
  }
1✔
493

494
  private Integer extractSumAggregationValue(Aggregate aggregation, String sumAggregationName) {
495
    if (aggregation != null && aggregation.isFilter()) {
5!
496
      var metric = aggregation.filter().aggregations().get(sumAggregationName);
7✔
497
      if (metric != null && metric.isSum()) {
5!
498
        return (int) Math.round(metric.sum().value());
8✔
499
      }
500
    }
501
    return 0;
×
502
  }
503
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc