Skip to content

Conversation

@tahminator
Copy link
Owner

@tahminator tahminator commented Jan 22, 2026

Description of changes

Checklist before review

  • I have done a thorough self-review of the PR
  • Copilot has reviewed my latest changes, and all comments have been fixed and/or closed.
  • If I have made database changes, I have made sure I followed all the db repo rules listed in the wiki here. (check if no db changes)
  • All tests have passed
  • I have successfully deployed this PR to staging
  • I have done manual QA in both dev (and staging if possible) and attached screenshots below.

Screenshots

Dev:
image
image

Staging:
image

@github-actions
Copy link
Contributor

Available PR Commands

  • /ai - Triggers all AI review commands at once
  • /review - AI review of the PR changes
  • /describe - AI-powered description of the PR
  • /improve - AI-powered suggestions
  • /deploy - Deploy to staging

See: https://github.com/tahminator/codebloom/wiki/CI-Commands

@github-actions
Copy link
Contributor

Title

659: Add metrics to LeetcodeClientImpl, ThrottledLeetcodeClientImpl and LeetcodeAuthStealer


PR Type

Enhancement


Description

  • Add Micrometer timing to clients

  • Time throttled client executions

  • Time auth cookie stealing flow


Diagram Walkthrough

flowchart LR
  A["LeetcodeClientImpl"] -- "@Timed leetcode.client.execution" --> M["Micrometer Metrics"]
  B["ThrottledLeetcodeClientImpl"] -- "@Timed leetcode.throttled.client.execution" --> M
  C["LeetcodeAuthStealer.stealCookieImpl"] -- "@Timed leetcode.auth.stealer.execution" --> M
Loading

File Walkthrough

Relevant files
Enhancement
LeetcodeClientImpl.java
Add execution timing to LeetcodeClientImpl                             

src/main/java/org/patinanetwork/codebloom/common/leetcode/LeetcodeClientImpl.java

  • Import Micrometer Timed annotation
  • Add @timed("leetcode.client.execution") to class
+2/-0     
ThrottledLeetcodeClientImpl.java
Time executions in throttled client                                           

src/main/java/org/patinanetwork/codebloom/common/leetcode/throttled/ThrottledLeetcodeClientImpl.java

  • Import Micrometer Timed annotation
  • Add @timed("leetcode.throttled.client.execution") to class
+2/-0     
LeetcodeAuthStealer.java
Time auth cookie stealing execution                                           

src/main/java/org/patinanetwork/codebloom/scheduled/auth/LeetcodeAuthStealer.java

  • Import Micrometer Counted and Timed
  • Add @timed("leetcode.auth.stealer.execution") to stealCookieImpl
+4/-0     

@github-actions
Copy link
Contributor

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 2 🔵🔵⚪⚪⚪
🧪 No relevant tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Metrics Scope

The class-level @timed annotation will time every public method; verify this matches the acceptance criteria and doesn't double-count if methods call each other or are proxied. Consider naming/percentiles/tags for better observability.

/** TODO: Add an input to determine whether the request must be processed quickly or not. */
@Component
@Primary
@Timed(value = "leetcode.client.execution")
public class LeetcodeClientImpl implements LeetcodeClient {
Metrics Granularity

@timed on the throttled client may conflate time spent waiting for the rate limiter with actual LeetCode request time; consider separate timers or tags to distinguish queue wait vs execution to aid performance analysis.

@Component
@Timed(value = "leetcode.throttled.client.execution")
public class ThrottledLeetcodeClientImpl extends LeetcodeClientImpl implements ThrottledLeetcodeClient {

    private static final long REQUESTS_OVER_TIME = 1L;
    private static final long MILLISECONDS_TO_WAIT = 100L;
Missing Counter Use

@counted is imported but unused; if counting attempts/success/failures is part of the task, add @counted with meaningful names and tags, or remove the unused import.

import io.micrometer.core.annotation.Counted;
import io.micrometer.core.annotation.Timed;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Map;

@tahminator tahminator force-pushed the 659 branch 3 times, most recently from 83b31f3 to 1918430 Compare January 22, 2026 03:51
@tahminator
Copy link
Owner Author

/ai

@tahminator
Copy link
Owner Author

/review

@tahminator
Copy link
Owner Author

/describe

@tahminator
Copy link
Owner Author

/improve

@github-actions
Copy link
Contributor

Title

659: Add metrics to LeetcodeClientImpl, ThrottledLeetcodeClientImpl and LeetcodeAuthStealer


PR Type

Enhancement


Description

  • Add Micrometer @timed to LeetCode client

  • Add timing to throttled client execution

  • Add long task timer to auth stealer


Diagram Walkthrough

flowchart LR
  A["LeetcodeClientImpl @Timed(leetcdoe.client.execution)"] -- "base execution timing" --> C["Micrometer metrics"]
  B["ThrottledLeetcodeClientImpl @Timed(leetcode.throttled.client.execution)"] -- "throttled execution timing" --> C
  D["LeetcodeAuthStealer.stealCookieImpl @Timed(longTask)"] -- "auth flow duration" --> C
Loading

File Walkthrough

Relevant files
Enhancement
LeetcodeClientImpl.java
Add timing to base LeetCode client                                             

src/main/java/org/patinanetwork/codebloom/common/leetcode/LeetcodeClientImpl.java

  • Import Micrometer Timed annotation.
  • Annotate class with @Timed("leetcode.client.execution").
+2/-0     
ThrottledLeetcodeClientImpl.java
Time throttled LeetCode client execution                                 

src/main/java/org/patinanetwork/codebloom/common/leetcode/throttled/ThrottledLeetcodeClientImpl.java

  • Import Micrometer Timed annotation.
  • Annotate class with @Timed("leetcode.throttled.client.execution").
+2/-0     
LeetcodeAuthStealer.java
Long task timing for auth cookie stealing                               

src/main/java/org/patinanetwork/codebloom/scheduled/auth/LeetcodeAuthStealer.java

  • Import Micrometer Timed annotation.
  • Annotate stealCookieImpl with @Timed(..., longTask = true).
+2/-0     

@github-actions
Copy link
Contributor

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 2 🔵🔵⚪⚪⚪
🧪 No relevant tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Metrics Naming

Confirm the metric name 'leetcode.client.execution' aligns with your Micrometer naming conventions and existing dashboards. Consider adding tags (e.g., method, status) for better cardinality and filtering if supported by your usage.

@Timed(value = "leetcode.client.execution")
public class LeetcodeClientImpl implements LeetcodeClient {
Overlapping Timers

The subclass adds its own @timed which may create overlapping timers with the parent class if parent methods are also timed. Validate that this doesn’t double-count or distort latency metrics, or add tags rather than a separate timer.

@Timed(value = "leetcode.throttled.client.execution")
public class ThrottledLeetcodeClientImpl extends LeetcodeClientImpl implements ThrottledLeetcodeClient {
Long Task Timer

Using longTask = true is appropriate for Playwright flows; ensure the registry supports LongTaskTimer and that task lifecycle is bounded (lock acquisition and release paths) to avoid stuck active tasks on exceptions.

@Timed(value = "leetcode.auth.stealer.execution", longTask = true)
String stealCookieImpl() {

@codecov
Copy link

codecov bot commented Jan 22, 2026

Codecov Report

❌ Patch coverage is 58.57843% with 169 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
.../codebloom/common/leetcode/LeetcodeClientImpl.java 64.21% 89 Missing and 13 partials ⚠️
.../codebloom/scheduled/auth/LeetcodeAuthStealer.java 45.08% 62 Missing and 5 partials ⚠️

📢 Thoughts on this report? Let us know!

@tahminator
Copy link
Owner Author

/deploy

@github-actions
Copy link
Contributor

The command to deploy to staging for the commit 1918430 has been triggered. View action run

@github-actions
Copy link
Contributor

Staging deployment succeeded for commit 1918430

View run

@tahminator
Copy link
Owner Author

/deploy

@github-actions
Copy link
Contributor

The command to deploy to staging for the commit da0f742 has been triggered. View action run

@github-actions
Copy link
Contributor

Staging deployment succeeded for commit da0f742

View run

@tahminator
Copy link
Owner Author

/deploy

@github-actions
Copy link
Contributor

The command to deploy to staging for the commit 876fd45 has been triggered. View action run

@github-actions
Copy link
Contributor

Staging deployment succeeded for commit 876fd45

View run

@tahminator
Copy link
Owner Author

/deploy

1 similar comment
@tahminator
Copy link
Owner Author

/deploy

@tahminator
Copy link
Owner Author

/review

@tahminator
Copy link
Owner Author

/describe

@tahminator
Copy link
Owner Author

/improve

@github-actions
Copy link
Contributor

Title

659: Add metrics to LeetcodeClientImpl, ThrottledLeetcodeClientImpl and LeetcodeAuthStealer


PR Type

Enhancement


Description

  • Add Micrometer timing to Leetcode client

  • Introduce reusable CustomTimer utility

  • Replace custom log timing with @Timed

  • Wire timer through throttled client


Diagram Walkthrough

flowchart LR
  CustomTimer["CustomTimer (Micrometer timer)"]
  LeetcodeClientImpl["LeetcodeClientImpl: wrap calls with timer"]
  ThrottledClient["ThrottledLeetcodeClientImpl: pass timer to super"]
  AuthStealer["LeetcodeAuthStealer: @Timed long task"]
  AuthSuccessHandler["@Timed onAuthenticationSuccess"]

  CustomTimer -- "injected" --> LeetcodeClientImpl
  LeetcodeClientImpl -- "used by" --> ThrottledClient
  AuthStealer -- "timed by" --> CustomTimer
  AuthSuccessHandler -- "Micrometer @Timed" --> CustomTimer
Loading

File Walkthrough

Relevant files
Enhancement
CustomAuthenticationSuccessHandler.java
Use Micrometer timing on auth success                                       

src/main/java/org/patinanetwork/codebloom/api/auth/security/CustomAuthenticationSuccessHandler.java

  • Add @Timed("controller.execution") to success handler
  • Remove @LogExecutionTime custom annotation
  • Import Micrometer Timed
+2/-2     
LeetcodeClientImpl.java
Time Leetcode client operations via CustomTimer                   

src/main/java/org/patinanetwork/codebloom/common/leetcode/LeetcodeClientImpl.java

  • Inject CustomTimer and add metric name
  • Wrap all public API calls with withTimer
  • Keep error handling, throttling, and parsing intact
  • Minor formatting while integrating timers
+391/-358
ThrottledLeetcodeClientImpl.java
Wire timer into throttled client constructor                         

src/main/java/org/patinanetwork/codebloom/common/leetcode/throttled/ThrottledLeetcodeClientImpl.java

  • Accept CustomTimer in constructor
  • Pass timer to superclass
  • Preserve rate-limiting behavior
+4/-2     
CustomTimer.java
Introduce reusable Micrometer timer utility                           

src/main/java/org/patinanetwork/codebloom/common/timer/CustomTimer.java

  • Add new CustomTimer component using Micrometer
  • Provide withTimer for Supplier and Runnable/Procedure
  • Auto-tag metrics with caller class and method
+47/-0   
LeetcodeAuthStealer.java
Time auth cookie stealing as long task                                     

src/main/java/org/patinanetwork/codebloom/scheduled/auth/LeetcodeAuthStealer.java

  • Add @Timed(value="leetcode.auth.stealer.execution", longTask=true)
  • Import Micrometer Timed
+2/-0     

@github-actions
Copy link
Contributor

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 No relevant tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Possible Issue

The Runnable-based withTimer path uses StackWalker.skip(2) while Supplier path uses skip(1). This asymmetry may misattribute class/method tags or fail under different inlining/stack depths; verify tags resolve to the caller consistently and won’t throw in production.

    public void withTimer(String metricName, Procedure procedure) {
        Runnable fn = () -> procedure.run();
        withTimer(metricName, fn);
    }

    private void withTimer(String metricName, Runnable runnable) {
        StackWalker.StackFrame caller = StackWalker.getInstance()
                .walk(frames -> frames.skip(2).findFirst())
                .orElseThrow();

        String className = caller.getClassName();
        String methodName = caller.getMethodName();

        var timer = meterRegistry.timer(metricName, "class", className, "method", methodName);

        timer.record(runnable);
    }
}
Error Handling

New wrapping with CustomTimer keeps prior behavior but throws generic RuntimeException messages; consider preserving context (status code, endpoint) and using typed exceptions to aid monitoring/alert triage.

@Override
public LeetcodeQuestion findQuestionBySlug(final String slug) {
    return customTimer.withTimer(TIMER_METRIC_NAME, () -> {
        String requestBody;
        try {
            requestBody = SelectProblemQuery.body(slug);
        } catch (Exception e) {
            throw new RuntimeException("Error building the request body");
        }

        try {
            HttpRequest request = getGraphQLRequestBuilder()
                    .POST(BodyPublishers.ofString(requestBody))
                    .build();

            HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
            int statusCode = response.statusCode();
            String body = response.body();

            if (statusCode != 200) {
                if (isThrottled(statusCode)) {
                    leetcodeAuthStealer.reloadCookie();
                }
                reporter.log(
                        "findQuestionBySlug",
                        Report.builder()
                                .data(String.format(
                                        """
                                Leetcode client failed to find question by slug due to status code of %d

                                Slug: %s

                                Body: %s

                                Header(s): %s
                            """,
                                        statusCode,
                                        slug,
                                        body,
                                        response.headers().toString()))
                                .build());
                throw new RuntimeException("API Returned status " + statusCode + ": " + body);
            }

            JsonNode node = mapper.readTree(body);

            int questionId =
                    node.path("data").path("question").path("questionId").asInt();
            String questionTitle =
                    node.path("data").path("question").path("title").asText();
            String titleSlug =
                    node.path("data").path("question").path("titleSlug").asText();
            String link = "https://leetcode.com/problems/" + titleSlug;
            String difficulty =
                    node.path("data").path("question").path("difficulty").asText();
            String question =
                    node.path("data").path("question").path("content").asText();

            String statsJson =
                    node.path("data").path("question").path("stats").asText();
            JsonNode stats = mapper.readTree(statsJson);
            String acRateString = stats.get("acRate").asText();
            float acRate = Float.parseFloat(acRateString.replace("%", "")) / 100f;

            JsonNode topicTagsNode = node.path("data").path("question").path("topicTags");

            List<LeetcodeTopicTag> tags = new ArrayList<>();

            for (JsonNode el : topicTagsNode) {
                tags.add(LeetcodeTopicTag.builder()
                        .name(el.get("name").asText())
                        .slug(el.get("slug").asText())
                        .build());
            }

            return LeetcodeQuestion.builder()
                    .link(link)
                    .questionId(questionId)
                    .questionTitle(questionTitle)
                    .titleSlug(titleSlug)
                    .difficulty(difficulty)
                    .question(question)
                    .acceptanceRate(acRate)
                    .topics(tags)
                    .build();
        } catch (Exception e) {
            throw new RuntimeException("Error fetching the API", e);
        }
    });
}

@Override
public ArrayList<LeetcodeSubmission> findSubmissionsByUsername(final String username) {
    return findSubmissionsByUsername(username, 20);
}

@Override
public ArrayList<LeetcodeSubmission> findSubmissionsByUsername(final String username, final int limit) {
    return customTimer.withTimer(TIMER_METRIC_NAME, () -> {
        ArrayList<LeetcodeSubmission> submissions = new ArrayList<>();

        String requestBody;
        try {
            requestBody = SelectAcceptedSubmisisonsQuery.body(username, limit);
        } catch (Exception e) {
            throw new RuntimeException("Error building the request body");
        }

        try {
            HttpRequest request = getGraphQLRequestBuilder()
                    .POST(BodyPublishers.ofString(requestBody))
                    .build();

            HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
            int statusCode = response.statusCode();
            String body = response.body();

            if (statusCode != 200) {
                if (isThrottled(statusCode)) {
                    leetcodeAuthStealer.reloadCookie();
                }
                reporter.log(
                        "findSubmissionsByUsername",
                        Report.builder()
                                .data(String.format(
                                        """
                                Leetcode client failed to find submission by username due to status code of %d

                                Username: %s

                                Body: %s

                                Header(s): %s
                            """,
                                        statusCode,
                                        username,
                                        body,
                                        response.headers().toString()))
                                .build());
                throw new RuntimeException("API Returned status " + statusCode + ": " + body);
            }

            JsonNode node = mapper.readTree(body);
            JsonNode submissionsNode = node.path("data").path("recentAcSubmissionList");

            if (submissionsNode.isArray()) {
                if (submissionsNode.isEmpty() || submissionsNode == null) {
                    return submissions;
                }

                for (JsonNode submission : submissionsNode) {
                    int id = submission.path("id").asInt();
                    String title = submission.path("title").asText();
                    String titleSlug = submission.path("titleSlug").asText();
                    String timestampString = submission.path("timestamp").asText();
                    long epochSeconds = Long.parseLong(timestampString);
                    Instant instant = Instant.ofEpochSecond(epochSeconds);

                    LocalDateTime timestamp = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
                    String statusDisplay = submission.path("statusDisplay").asText();
                    submissions.add(new LeetcodeSubmission(id, title, titleSlug, timestamp, statusDisplay));
                }
            }

            return submissions;
        } catch (Exception e) {
            throw new RuntimeException("Error fetching the API", e);
        }
    });
}

@Override
public LeetcodeDetailedQuestion findSubmissionDetailBySubmissionId(final int submissionId) {
    return customTimer.withTimer(TIMER_METRIC_NAME, () -> {
        String requestBody;
        try {
            requestBody = GetSubmissionDetails.body(submissionId);
        } catch (Exception e) {
            throw new RuntimeException("Error building the request body");
        }

        try {
            HttpRequest request = getGraphQLRequestBuilder()
                    .POST(BodyPublishers.ofString(requestBody))
                    .build();

            HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
            int statusCode = response.statusCode();
            String body = response.body();

            if (statusCode != 200) {
                if (isThrottled(statusCode)) {
                    leetcodeAuthStealer.reloadCookie();
                }
                reporter.log(
                        "findSubmissionDetailBySubmissionId",
                        Report.builder()
                                .data(String.format(
                                        """
                                Leetcode client failed to find submission detail by submission ID due to status code of %d

                                Submission ID: %s

                                Body: %s

                                Header(s): %s
                            """,
                                        statusCode,
                                        submissionId,
                                        body,
                                        response.headers().toString()))
                                .build());
                throw new RuntimeException("API Returned status " + statusCode + ": " + body);
            }

            JsonNode node = mapper.readTree(body);
            JsonNode baseNode = node.path("data").path("submissionDetails");

            int runtime = baseNode.path("runtime").asInt();
            String runtimeDisplay = baseNode.path("runtimeDisplay").asText();
            float runtimePercentile =
                    (float) baseNode.path("runtimePercentile").asDouble();
            int memory = baseNode.path("memory").asInt();
            String memoryDisplay = baseNode.path("memoryDisplay").asText();
            float memoryPercentile =
                    (float) baseNode.path("memoryPercentile").asDouble();
            String code = baseNode.path("code").asText();
            String langName = baseNode.path("lang").path("name").asText();
            String langVerboseName =
                    baseNode.path("lang").path("verboseName").asText();
            Lang lang = (Strings.isNullOrEmpty(langName) || Strings.isNullOrEmpty(langVerboseName))
                    ? null
                    : new Lang(langName, langVerboseName);

            // if any of these are empty, then extremely likely that we're throttled.
            if (Strings.isNullOrEmpty(runtimeDisplay) || Strings.isNullOrEmpty(memoryDisplay)) {
                leetcodeAuthStealer.reloadCookie();
            }

            LeetcodeDetailedQuestion question = new LeetcodeDetailedQuestion(
                    runtime,
                    runtimeDisplay,
                    runtimePercentile,
                    memory,
                    memoryDisplay,
                    memoryPercentile,
                    code,
                    lang);

            return question;
        } catch (Exception e) {
            throw new RuntimeException("Error fetching the API", e);
        }
    });
}

public POTD getPotd() {
    return customTimer.withTimer(TIMER_METRIC_NAME, () -> {
        String requestBody;
        try {
            requestBody = GetPotd.body();
        } catch (Exception e) {
            throw new RuntimeException("Error building the request body");
        }

        try {
            HttpRequest request = getGraphQLRequestBuilder()
                    .POST(BodyPublishers.ofString(requestBody))
                    .build();

            HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
            int statusCode = response.statusCode();
            String body = response.body();

            if (statusCode != 200) {
                if (isThrottled(statusCode)) {
                    leetcodeAuthStealer.reloadCookie();
                }
                reporter.log(
                        "getPotd",
                        Report.builder()
                                .data(String.format(
                                        """
                                Leetcode client failed to get POTD due to status code of %d

                                Body: %s

                                Header(s): %s
                            """,
                                        statusCode, body, response.headers().toString()))
                                .build());
                throw new RuntimeException("API Returned status " + statusCode + ": " + body);
            }

            JsonNode node = mapper.readTree(body);
            JsonNode baseNode = node.path("data")
                    .path("activeDailyCodingChallengeQuestion")
                    .path("question");

            String titleSlug = baseNode.path("titleSlug").asText();
            String title = baseNode.path("title").asText();
            var difficulty =
                    QuestionDifficulty.valueOf(baseNode.path("difficulty").asText());

            return new POTD(title, titleSlug, difficulty);
        } catch (Exception e) {
            throw new RuntimeException("Error fetching the API", e);
        }
    });
}

@Override
public UserProfile getUserProfile(final String username) {
    return customTimer.withTimer(TIMER_METRIC_NAME, () -> {
        String requestBody;
        try {
            requestBody = GetUserProfile.body(username);
        } catch (Exception e) {
            throw new RuntimeException("Error building the request body", e);
        }

        try {
            HttpRequest request = getGraphQLRequestBuilder()
                    .POST(BodyPublishers.ofString(requestBody))
                    .build();

            HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
            int statusCode = response.statusCode();
            String body = response.body();

            if (statusCode != 200) {
                if (isThrottled(statusCode)) {
                    leetcodeAuthStealer.reloadCookie();
                }
                reporter.log(
                        "getUserProfile",
                        Report.builder()
                                .data(String.format(
                                        """
                                Leetcode client failed to get user profile by username due to status code of %d

                                Username: %s

                                Body: %s

                                Header(s): %s
                            """,
                                        statusCode,
                                        username,
                                        body,
                                        response.headers().toString()))
                                .build());
                throw new RuntimeException("API Returned status " + statusCode + ": " + body);
            }

            JsonNode node = mapper.readTree(body);
            JsonNode baseNode = node.path("data").path("matchedUser");

            var returnedUsername = baseNode.path("username").asText();
            var ranking = baseNode.path("profile").path("ranking").asText();
            var userAvatar = baseNode.path("profile").path("userAvatar").asText();
            var realName = baseNode.path("profile").path("realName").asText();
            var aboutMe = baseNode.path("profile").path("aboutMe").asText().trim();

            return new UserProfile(returnedUsername, ranking, userAvatar, realName, aboutMe);
        } catch (Exception e) {
            throw new RuntimeException("Error fetching the API", e);
        }
    });
}

@Override
public Set<LeetcodeTopicTag> getAllTopicTags() {
    return customTimer.withTimer(TIMER_METRIC_NAME, () -> {
        try {
            HttpRequest request = getGraphQLRequestBuilder()
                    .POST(BodyPublishers.ofString(GetTopics.body()))
                    .build();

            HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
            int statusCode = response.statusCode();
            String body = response.body();

            if (statusCode != 200) {
                if (isThrottled(statusCode)) {
                    leetcodeAuthStealer.reloadCookie();
                }
                reporter.log(
                        "getAllTopicTags",
                        Report.builder()
                                .data(String.format(
                                        """
                                Leetcode client failed to get all topic tags due to status code of %d

                                Body: %s

                                Header(s): %s
                            """,
                                        statusCode, body, response.headers().toString()))
                                .build());
                throw new RuntimeException(
                        "Non-successful response getting topics from Leetcode API. Status code: " + statusCode);
            }

            JsonNode json = mapper.readTree(body);
            JsonNode edges = json.path("data").path("questionTopicTags").path("edges");

            if (!edges.isArray()) {
                throw new RuntimeException("The expected shape of getting topics did not match the received body");
            }

            Set<LeetcodeTopicTag> result = new HashSet<>();

            for (JsonNode edge : edges) {
                JsonNode node = edge.path("node");
                result.add(LeetcodeTopicTag.builder()
                        .name(node.get("name").asText())
                        .slug(node.get("slug").asText())
                        .build());
            }

            return result;
        } catch (Exception e) {
            throw new RuntimeException("Error getting topics from Leetcode API", e);
        }
    });
}

public List<LeetcodeQuestion> getAllProblems() {
    return customTimer.withTimer(TIMER_METRIC_NAME, () -> {
        try {
            HttpRequest request = getGraphQLRequestBuilder()
                    .POST(BodyPublishers.ofString(GetAllProblems.body()))
                    .build();
            HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
            int statusCode = response.statusCode();
            String body = response.body();
            if (statusCode != 200) {
                if (isThrottled(statusCode)) {
                    leetcodeAuthStealer.reloadCookie();
                }
                reporter.log(
                        "getAllProblems",
                        Report.builder()
                                .data(String.format(
                                        """
                                Leetcode client failed to get all leetcode questions due to status code of %d

                                Body: %s

                                Header(s): %s
                            """,
                                        statusCode, body, response.headers().toString()))
                                .build());
                throw new RuntimeException(
                        "Non-successful response getting all questions from Leetcode API. Status code: "
                                + statusCode);
            }

            JsonNode json = mapper.readTree(body);
            JsonNode allQuestions =
                    json.path("data").path("problemsetQuestionListV2").path("questions");

            if (!allQuestions.isArray()) {
                throw new RuntimeException("The expected shape of getting topics did not match the received body");
            }

            List<LeetcodeQuestion> result = new ArrayList<>();
            for (JsonNode question : allQuestions) {
                JsonNode topicTags = question.get("topicTags");

                List<LeetcodeTopicTag> tags = new ArrayList<>();
                for (JsonNode tag : topicTags) {
                    tags.add(LeetcodeTopicTag.builder()
                            .name(tag.get("name").asText())
                            .slug(tag.get("slug").asText())
                            .build());
                }

                result.add(LeetcodeQuestion.builder()
                        .link("https://leetcode.com/problems/"
                                + question.get("titleSlug").asText())
                        .questionId(question.get("questionFrontendId").asInt())
                        .questionTitle(question.get("title").asText())
                        .titleSlug(question.get("titleSlug").asText())
                        .difficulty(question.get("difficulty").asText())
                        .acceptanceRate((float) question.get("acRate").asDouble())
                        .topics(tags)
                        .build());
            }
            return result;
        } catch (Exception e) {
            throw new RuntimeException("Error getting all problems from Leetcode API", e);
        }
    });
}
Metrics Consistency

Replaced custom LogExecutionTime with @timed("controller.execution") on auth success; ensure this matches existing metric naming conventions and required tags, and verify removal of Discord reporting is intentional.

@Timed(value = "controller.execution")
public void onAuthenticationSuccess(
        final HttpServletRequest request, final HttpServletResponse response, final Authentication authentication)
        throws IOException, ServletException {
    Object principal = authentication.getPrincipal();

@tahminator
Copy link
Owner Author

/deploy

@github-actions
Copy link
Contributor

The command to deploy to staging for the commit 876fd45 has been triggered. View action run

@github-actions
Copy link
Contributor

Staging deployment succeeded for commit 876fd45

View run

@tahminator tahminator force-pushed the 659 branch 2 times, most recently from 7d2a36e to 3ab5a15 Compare January 24, 2026 08:48
@tahminator
Copy link
Owner Author

/deploy

@github-actions
Copy link
Contributor

The command to deploy to staging for the commit 3ab5a15 has been triggered. View action run

@github-actions
Copy link
Contributor

Staging deployment succeeded for commit 3ab5a15

View run

@tahminator
Copy link
Owner Author

/deploy

@github-actions
Copy link
Contributor

The command to deploy to staging for the commit cca1c21 has been triggered. View action run

@tahminator
Copy link
Owner Author

/deploy

@github-actions
Copy link
Contributor

Staging deployment was cancelled for commit cca1c21

View run

@github-actions
Copy link
Contributor

The command to deploy to staging for the commit 0da664d has been triggered. View action run

@tahminator
Copy link
Owner Author

/ai

@tahminator
Copy link
Owner Author

/review

@tahminator
Copy link
Owner Author

/describe

@tahminator
Copy link
Owner Author

/improve

@github-actions
Copy link
Contributor

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 No relevant tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Metrics Tagging

Using StackWalker for tags on every call may add overhead; consider caching tags per method or using @timed annotations for consistency and lower cost.

private Timer timer() {
    var stackFrame = StackWalker.getInstance()
            .walk(frames -> frames.skip(1).findFirst())
            .orElseThrow();

    String methodName = stackFrame.getMethodName();
    String className = stackFrame.getClassName();

    return meterRegistry.timer(METRIC_NAME, "class", className, "method", methodName);
}
Async Timing

Wrapping CompletableFuture creation in timer().record() measures only creation, not async work; consider Timer.Sample to time actual execution or annotate methods with @timed longTask for async tasks.

@Async
public CompletableFuture<Optional<String>> reloadCookie() {
    return timer().record(() -> CompletableFuture.completedFuture(Optional.ofNullable(stealCookieImpl())));
}
Metric Naming

@timed uses a generic name 'controller.execution'; ensure alignment with existing metric naming conventions and consider adding tags (controller, method) for cardinality control.

@Timed(value = "controller.execution")
public void onAuthenticationSuccess(
        final HttpServletRequest request, final HttpServletResponse response, final Authentication authentication)

@github-actions
Copy link
Contributor

Title

659: Add metrics to LeetcodeClientImpl, ThrottledLeetcodeClientImpl and LeetcodeAuthStealer


PR Type

Enhancement


Description

  • Add Micrometer timing to auth handler

  • Instrument Leetcode client methods with timers

  • Propagate MeterRegistry through constructors

  • Time auth cookie stealing and accessors


Diagram Walkthrough

flowchart LR
  MR["MeterRegistry injected"]
  LCI["LeetcodeClientImpl methods timed"]
  TLCI["ThrottledLeetcodeClientImpl ctor updated"]
  LAS["LeetcodeAuthStealer methods timed"]
  CSH["CustomAuthenticationSuccessHandler @Timed"]
  MR -- "constructor injection" --> LCI
  MR -- "constructor injection" --> TLCI
  MR -- "constructor injection" --> LAS
  LCI -- "record() around operations" --> "Timers 'leetcode.client.execution'"
  LAS -- "record() around auth ops" --> "Timers 'leetcode.client.execution'"
  CSH -- "@Timed('controller.execution')" --> "Controller timing"
Loading

File Walkthrough

Relevant files
Enhancement
CustomAuthenticationSuccessHandler.java
Time authentication success handler execution                       

src/main/java/org/patinanetwork/codebloom/api/auth/security/CustomAuthenticationSuccessHandler.java

  • Add Micrometer @timed annotation
  • Replace custom LogExecutionTime with @timed
  • Import Micrometer annotation
+2/-2     
LeetcodeClientImpl.java
Instrument Leetcode client methods with timers                     

src/main/java/org/patinanetwork/codebloom/common/leetcode/LeetcodeClientImpl.java

  • Inject MeterRegistry and create timer helper
  • Wrap public API methods with timer.record()
  • Define metric name 'leetcode.client.execution'
  • Adjust constructors to accept MeterRegistry
+441/-396
ThrottledLeetcodeClientImpl.java
Propagate MeterRegistry to throttled client                           

src/main/java/org/patinanetwork/codebloom/common/leetcode/throttled/ThrottledLeetcodeClientImpl.java

  • Accept MeterRegistry in constructor
  • Pass MeterRegistry to superclass
+4/-2     
LeetcodeAuthStealer.java
Add timing to Leetcode auth stealing workflow                       

src/main/java/org/patinanetwork/codebloom/scheduled/auth/LeetcodeAuthStealer.java

  • Inject MeterRegistry and add timer helper
  • Time steal, reload, getCookie, getCsrf, and impl methods
  • Use common metric 'leetcode.client.execution' with tags
+169/-143

@github-actions
Copy link
Contributor

Staging deployment succeeded for commit 0da664d

View run

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