Skip to content

Monerh A#3

Open
Miuroro wants to merge 8 commits into
HackYourAssignment:mainfrom
Miuroro:main
Open

Monerh A#3
Miuroro wants to merge 8 commits into
HackYourAssignment:mainfrom
Miuroro:main

Conversation

@Miuroro

@Miuroro Miuroro commented May 28, 2026

Copy link
Copy Markdown

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

  • Initialized Spring Boot Maven project structure for Task 1
  • Added main application entry point
  • Added analytics controller
  • Added analytics service
  • Added analytics domain model
  • Added request, response, and summary DTOs
  • Added ResourceNotFoundException
  • Added GlobalExceptionHandler
  • Added application configuration
  • Added API design documentation
  • Added initial test class

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.

validateTimeRange(startTime, endTime);

return records.values().stream()
.filter(record -> matchesTextFilter(record.getEventType(), eventType))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i didnt know its a keyword, i'll fix it and i'll keep this in mind thank you

@cometbroom cometbroom left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?
Image

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok im changing that

}

private ResponseEntity<Map<String, Object>> buildErrorResponse(HttpStatus status, String message, String path) {
Map<String, Object> body = new LinkedHashMap<>();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very well done on the filters here too 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants