Monerh A#3
Conversation
…TOs, service, controller, and exception handling
| validateTimeRange(startTime, endTime); | ||
|
|
||
| return records.values().stream() | ||
| .filter(record -> matchesTextFilter(record.getEventType(), eventType)) |
There was a problem hiding this comment.
Minor issue. "record" is a keyword in Java for record types which are immutable classes with getters and setters and a canonic constructor already included. Keywords such as "class", "int" etc... can't be used as names for variables. Exception to the rule has been made for "record", "var" and "yield" because these are modern Java features (After Java 8) and to maintain backwards compatibility with older Java versions, are still allowed.
Nevertheless, while it's technically possible, better to name variables differently like "aRecord" to minimize possible confusion.
There was a problem hiding this comment.
i didnt know its a keyword, i'll fix it and i'll keep this in mind thank you
cometbroom
left a comment
There was a problem hiding this comment.
Impressive job on this one Monerh.
- Controllers are very appropriately thin with correct status codes.
- Super clean package structure (Small comment on naming convention to keep in mind for the future).
- Solid tests and test coverage (Also good to keep in mind for future Exception handlers can and are also often unit tested).
- Error handling is centralized with a consistent body
Added a few minor comments. Couple of them can already be resolved but good to know.
| .sorted(Comparator.comparing(AnalyticsRecord::getTimestamp).reversed() | ||
| .thenComparing(AnalyticsRecord::getId)) | ||
| .map(this::toResponse) | ||
| .collect(Collectors.toList()); |
There was a problem hiding this comment.
This can hide a bug that is difficult to diagnose.
.collect(Collectors.toList()); returns a mutable list. For a get endpoint you're certain that your list will no longer need to be modified. So it's better to use toList() which returns an immutable list.
| @RequestParam(required = false) | ||
| @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) OffsetDateTime startTime, | ||
| @RequestParam(required = false) | ||
| @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) OffsetDateTime endTime |
There was a problem hiding this comment.
Running the debugger with a breakpoint on line 47 shows startTime and endTime are coming as null when I passed them in below request
GET http://localhost:8080/api/v1/analytics?eventSource=mobile&eventType=page_view&sessionId=sess-1&startDate=2024-01-15&endDate=2025-01-16
This is due to no @RequestParam() annotation added for them. Can you please check?

There was a problem hiding this comment.
i see, i'll fix this and add both parameter names, but i dont know how to do alias handling if the user only input a date value only
There was a problem hiding this comment.
It's possible to accept both date and datetime values for the parameter but you either have to register a new formatter to Spring or accept a String and do manually formatting of dates to the start of day UTC (Eg: 10-06-2026 into 2026-06-10T00:00:00.000Z).
However, sometimes the flexibility you want to provide can become too complicated for the scope of the API. In those cases, since you're the one writing the contract, it is best to just disallow date-only values and expect the consumer to pass the datetime like 2026-06-10T00:00:00.000Z when they don't have a specific time in mind.
| import java.util.Map; | ||
|
|
||
| @Data | ||
| @NoArgsConstructor |
There was a problem hiding this comment.
Looking at AnalyticsSummary by ctrl+click, I can't find a noArgsConstructor usage. You could see this happening in older code where Jackson needed a noArgsConstructor and setters and getters for serialization and deserialization, but this is not the case anymore. Can you please try removing it?
There was a problem hiding this comment.
i was reading that using no_args constructor is a safer method to implement but didn't know its only for the older jackson versions, ill remove it
|
|
||
| @Data | ||
| @NoArgsConstructor | ||
| @AllArgsConstructor |
There was a problem hiding this comment.
Looking at your fields' usages (Also ctrl+click will also show setter method, ie: setTotalRecords), I also can't see setter methods being used. That means you can simplify this into:
@Data
public class AnalyticsSummary {
private final long totalRecords;
private final Map<String, Long> totalsByEventType;
private final long uniqueSessions;
}@Data will already have @RequiredArgsConstructor which will create a constructor for final fields.
| } | ||
|
|
||
| private ResponseEntity<Map<String, Object>> buildErrorResponse(HttpStatus status, String message, String path) { | ||
| Map<String, Object> body = new LinkedHashMap<>(); |
There was a problem hiding this comment.
First of all very well done on refactoring this outside 👍
A couple of issues on this. Minor one first, better to use Map.of() at this stage when response goes back to client to ensure your map is not mutated and easier writing.
Second is on having non-String types such as Object here for Map values. In that case Spring will automatically call toString() on that type to prepare it as a response body. If your type has a toString() method implemented correctly, that's fine and no issues. But once you pass a type that doesn't have toString(), first toString() of supertypes is called which will give you unexpected results.
In this case, best thing to do is to already here decide how you want each type to become string.
Second, it's much better to have your error response map type as Map<String, String>.
private ResponseEntity<Map<String, String>> buildErrorResponse(HttpStatus status, String message, String path) {
Map<String, String> body = new LinkedHashMap<>();
body.put("timestamp", OffsetDateTime.now().toString());
body.put("status", Integer.toString(status.value()));
body.put("error", status.getReasonPhrase());
body.put("message", message);
body.put("path", path);
return ResponseEntity.status(status).body(body);
}| .filter(record -> matchesTextFilter(record.getEventSource(), eventSource)) | ||
| .filter(record -> matchesTextFilter(record.getSessionId(), sessionId)) | ||
| .filter(record -> matchesStartTime(record.getTimestamp(), startTime)) | ||
| .filter(record -> matchesEndTime(record.getTimestamp(), endTime)) |
There was a problem hiding this comment.
Very well done on the filters here too 👍
…ized string-based responses
Summary
This PR adds the first complete version of the Analytics API using Spring Boot, including core endpoints, service logic, DTOs, model, and exception handling.
What Changed
Testing
Covers AnalyticsService flows for create, get/filter, get-by-id not found, replace (success/not found), delete (success/not found), summary (normal/empty/null sessions), invalid time range, boundary timestamps, blank/trimmed filters, and ordering logic.