Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Nov 16, 2025

The metrics system required changes across 5+ files to add a new metric type, with tight coupling between metric definitions and a 100+ line switch statement in the Prometheus exporter.

Changes

New Configuration Infrastructure

  • MetricConfiguration interface: Self-describing metric definitions with Prometheus metadata and data processing logic
  • MetricRegistry: Centralized registry managing metric configurations
  • metricConfigurations.ts: Single file containing all 14 metric definitions

Refactored Prometheus Exporter

  • Replaced switch statement with configuration-driven processing
  • Dynamic metric instantiation from configurations
  • Generic error handling per metric instead of exhaustive cases

Developer Experience

  • Added ADDING_METRICS.md with usage guide
  • Comprehensive test coverage for MetricRegistry

Example: Adding a New Metric

Before (5-6 file changes):

// 1. metrics.ts - Add enum
// 2. MetricDataMap.ts - Add type
// 3-6. prometheus.ts - Add MetricTypeMap entry, gauge definition, switch case, registration

After (2-3 file changes):

// 1-2. metrics.ts + MetricDataMap.ts (same as before)
// 3. metricConfigurations.ts - Add one object:
{
    name: Metrics.CACHE_ACCESS,
    prometheusType: PrometheusMetricType.COUNTER,
    prometheusName: "alphagamebot_cache_access_total",
    prometheusHelp: "Cache access hits and misses",
    prometheusLabels: ["cache_key", "result"],
    processData: (metric, data) => {
        const typed = data as MetricDataMap[Metrics.CACHE_ACCESS];
        (metric as Counter).inc({ 
            cache_key: typed.cacheKey,
            result: typed.hit ? "hit" : "miss"
        });
    }
}

Impact

  • Reduced complexity: ~50% fewer file changes, ~75% less code per metric
  • Backward compatible: No changes to metric submission API
  • All 65 tests passing, 0 security alerts

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • checkpoint.prisma.io
    • Triggering command: /usr/local/bin/node /home/REDACTED/work/AlphaGameBotJS/AlphaGameBotJS/node_modules/prisma/build/child {"product":"prisma","version":"6.19.0","cli_install_type":"local","information":"","local_timestamp":"2025-11-16T16:37:05Z","project_hash":"043ef723","cli_path":"/home/REDACTED/work/AlphaGameBotJS/AlphaGameBotJS/node_modules/.bin/prisma","cli_path_hash":"c67e5c4f","endpoint":"REDACTED","disable":false,"arch":"x64","os":"linux","node_version":"v20.19.5","ci":true,"ci_name":"GitHub Actions","command":"generate","schema_providers":["mysql"],"schema_preview_features":[],"schema_generators_providers":["prisma-client-js"],"cache_file":"/home/REDACTED/.cache/checkpoint-nodejs/prisma-c67e5c4f","cache_duration":43200000,"remind_duration":172800000,"force":false,"timeout":5000,"unref":true,"child_path":"/home/REDACTED/work/AlphaGameBotJS/AlphaGameBotJS/node_modules/prisma/build/child","client_event_id":"","previous_client_event_id":"","check_if_update_available":true} (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Original prompt

I want you to refactor the metrics system, making it so that it is easier and more scalable for more future metric types.


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Copilot AI and others added 2 commits November 16, 2025 16:37
Co-authored-by: AlphaGameDeveloper <77273893+AlphaGameDeveloper@users.noreply.github.com>
- Add MetricConfiguration interface for self-describing metrics
- Create MetricRegistry for centralized metric configuration management
- Implement configuration-driven Prometheus exporter
- Add comprehensive tests for MetricRegistry
- Create documentation for adding new metrics
- Eliminate large switch statement in prometheus exporter
- Reduce new metric addition from 5+ file changes to 2-3

Co-authored-by: AlphaGameDeveloper <77273893+AlphaGameDeveloper@users.noreply.github.com>
Copilot AI changed the title [WIP] Refactor metrics system for scalability and ease of use Refactor metrics system to configuration-driven architecture Nov 16, 2025
Copilot finished work on behalf of AlphaGameDeveloper November 16, 2025 16:50
@github-actions
Copy link

github-actions bot commented Nov 16, 2025

Coverage report

Caution

Test run failed

St.
Category Percentage Covered / Total
🟡 Statements
67.9% (+1.96% 🔼)
165/243
🔴 Branches
50.3% (+1.2% 🔼)
85/169
🟢 Functions
82.05% (+2.64% 🔼)
32/39
🟡 Lines
69.7% (+1.96% 🔼)
161/231
Show new covered files 🐣
St.
File Statements Branches Functions Lines
🟢
... / MetricConfiguration.ts
100% 100% 100% 100%
🟢
... / MetricRegistry.ts
100% 100% 100% 100%

Test suite run failed

Failed tests: 7/65. Failed suites: 1/9.
  ● crawlCommands › should return a Collection instance

    ENOENT: no such file or directory, scandir '/home/runner/work/Monorepo/Monorepo/src/commands'

      46 |
      47 |     // Read category folders (e.g., test/, utility/)
    > 48 |     const categoryFolders = readdirSync(commandsPath);
         |                             ^
      49 |
      50 |     for (const category of categoryFolders) {
      51 |         const categoryPath = path.join(commandsPath, category);

      at readdirSync (bot/src/utility/crawler.ts:48:29)
      at Object.<anonymous> (bot/src/utility/crawler.test.ts:39:32)

  ● crawlCommands › should load commands with proper structure

    ENOENT: no such file or directory, scandir '/home/runner/work/Monorepo/Monorepo/src/commands'

      46 |
      47 |     // Read category folders (e.g., test/, utility/)
    > 48 |     const categoryFolders = readdirSync(commandsPath);
         |                             ^
      49 |
      50 |     for (const category of categoryFolders) {
      51 |         const categoryPath = path.join(commandsPath, category);

      at readdirSync (bot/src/utility/crawler.ts:48:29)
      at Object.<anonymous> (bot/src/utility/crawler.test.ts:44:32)

  ● crawlCommands › should load the hworld test command if it exists

    ENOENT: no such file or directory, scandir '/home/runner/work/Monorepo/Monorepo/src/commands'

      46 |
      47 |     // Read category folders (e.g., test/, utility/)
    > 48 |     const categoryFolders = readdirSync(commandsPath);
         |                             ^
      49 |
      50 |     for (const category of categoryFolders) {
      51 |         const categoryPath = path.join(commandsPath, category);

      at readdirSync (bot/src/utility/crawler.ts:48:29)
      at Object.<anonymous> (bot/src/utility/crawler.test.ts:56:32)

  ● crawlCommands › should discover commands from nested category/commandname folders

    ENOENT: no such file or directory, scandir '/home/runner/work/Monorepo/Monorepo/src/commands'

      46 |
      47 |     // Read category folders (e.g., test/, utility/)
    > 48 |     const categoryFolders = readdirSync(commandsPath);
         |                             ^
      49 |
      50 |     for (const category of categoryFolders) {
      51 |         const categoryPath = path.join(commandsPath, category);

      at readdirSync (bot/src/utility/crawler.ts:48:29)
      at Object.<anonymous> (bot/src/utility/crawler.test.ts:67:32)

  ● crawlEvents › should return an Array

    ENOENT: no such file or directory, scandir '/home/runner/work/Monorepo/Monorepo/src/events'

      105 |     const eventsPath = existsSync(distEventsPath) ? distEventsPath : srcEventsPath;
      106 |     const eventsIsDist = eventsPath.includes(path.join(path.sep, "dist", path.sep)) || eventsPath.endsWith(path.join(path.sep, "dist"));
    > 107 |     const eventFiles = readdirSync(eventsPath).filter(file => eventsIsDist ? file.endsWith(".js") : file.endsWith(".ts") || file.endsWith(".js"));
          |                        ^
      108 |     const events: Array<LoadedEventHandler<keyof ClientEvents>> = [];
      109 |
      110 |     logger.debug(`Crawling events in: ${eventsPath}`);

      at readdirSync (bot/src/utility/crawler.ts:107:24)
      at Object.<anonymous> (bot/src/utility/crawler.test.ts:82:30)

  ● crawlEvents › should load events with proper structure

    ENOENT: no such file or directory, scandir '/home/runner/work/Monorepo/Monorepo/src/events'

      105 |     const eventsPath = existsSync(distEventsPath) ? distEventsPath : srcEventsPath;
      106 |     const eventsIsDist = eventsPath.includes(path.join(path.sep, "dist", path.sep)) || eventsPath.endsWith(path.join(path.sep, "dist"));
    > 107 |     const eventFiles = readdirSync(eventsPath).filter(file => eventsIsDist ? file.endsWith(".js") : file.endsWith(".ts") || file.endsWith(".js"));
          |                        ^
      108 |     const events: Array<LoadedEventHandler<keyof ClientEvents>> = [];
      109 |
      110 |     logger.debug(`Crawling events in: ${eventsPath}`);

      at readdirSync (bot/src/utility/crawler.ts:107:24)
      at Object.<anonymous> (bot/src/utility/crawler.test.ts:87:30)

  ● crawlEvents › should load the InteractionCreate event if it exists

    ENOENT: no such file or directory, scandir '/home/runner/work/Monorepo/Monorepo/src/events'

      105 |     const eventsPath = existsSync(distEventsPath) ? distEventsPath : srcEventsPath;
      106 |     const eventsIsDist = eventsPath.includes(path.join(path.sep, "dist", path.sep)) || eventsPath.endsWith(path.join(path.sep, "dist"));
    > 107 |     const eventFiles = readdirSync(eventsPath).filter(file => eventsIsDist ? file.endsWith(".js") : file.endsWith(".ts") || file.endsWith(".js"));
          |                        ^
      108 |     const events: Array<LoadedEventHandler<keyof ClientEvents>> = [];
      109 |
      110 |     logger.debug(`Crawling events in: ${eventsPath}`);

      at readdirSync (bot/src/utility/crawler.ts:107:24)
      at Object.<anonymous> (bot/src/utility/crawler.test.ts:100:30)

Report generated by 🧪jest coverage report action from 65d0d98

Copilot stopped work on behalf of AlphaGameDeveloper due to an error November 16, 2025 19:13
Copilot stopped work on behalf of AlphaGameDeveloper due to an error November 16, 2025 19:16
@AlphaGameDeveloper
Copy link
Member

AlphaGameDeveloper commented Nov 16, 2025

@copilot The problem here seems to be the fact that it is looking in (project root)/src/commands for the commands when doing tests. When I run npm -w bot t on my machine, it works completely fine and all tests pass. It seems to be an issue with the workflow, from what I can see. I doubt it's the code. Would you mind taking a look at that?

The tests that were not successful in CI were, specifically:

These:
  ● crawlCommands › should return a Collection instance

    ENOENT: no such file or directory, scandir '/home/runner/work/Monorepo/Monorepo/src/commands'

      46 |
      47 |     // Read category folders (e.g., test/, utility/)
    > 48 |     const categoryFolders = readdirSync(commandsPath);
         |                             ^
      49 |
      50 |     for (const category of categoryFolders) {
      51 |         const categoryPath = path.join(commandsPath, category);

      at readdirSync (bot/src/utility/crawler.ts:48:29)
      at Object.<anonymous> (bot/src/utility/crawler.test.ts:39:32)

  ● crawlCommands › should load commands with proper structure

    ENOENT: no such file or directory, scandir '/home/runner/work/Monorepo/Monorepo/src/commands'

      46 |
      47 |     // Read category folders (e.g., test/, utility/)
    > 48 |     const categoryFolders = readdirSync(commandsPath);
         |                             ^
      49 |
      50 |     for (const category of categoryFolders) {
      51 |         const categoryPath = path.join(commandsPath, category);

      at readdirSync (bot/src/utility/crawler.ts:48:29)
      at Object.<anonymous> (bot/src/utility/crawler.test.ts:44:32)

  ● crawlCommands › should load the hworld test command if it exists

    ENOENT: no such file or directory, scandir '/home/runner/work/Monorepo/Monorepo/src/commands'

      46 |
      47 |     // Read category folders (e.g., test/, utility/)
    > 48 |     const categoryFolders = readdirSync(commandsPath);
         |                             ^
      49 |
      50 |     for (const category of categoryFolders) {
      51 |         const categoryPath = path.join(commandsPath, category);

      at readdirSync (bot/src/utility/crawler.ts:48:29)
      at Object.<anonymous> (bot/src/utility/crawler.test.ts:56:32)

  ● crawlCommands › should discover commands from nested category/commandname folders

    ENOENT: no such file or directory, scandir '/home/runner/work/Monorepo/Monorepo/src/commands'

      46 |
      47 |     // Read category folders (e.g., test/, utility/)
    > 48 |     const categoryFolders = readdirSync(commandsPath);
         |                             ^
      49 |
      50 |     for (const category of categoryFolders) {
      51 |         const categoryPath = path.join(commandsPath, category);

      at readdirSync (bot/src/utility/crawler.ts:48:29)
      at Object.<anonymous> (bot/src/utility/crawler.test.ts:67:32)

  ● crawlEvents › should return an Array

    ENOENT: no such file or directory, scandir '/home/runner/work/Monorepo/Monorepo/src/events'

      105 |     const eventsPath = existsSync(distEventsPath) ? distEventsPath : srcEventsPath;
      106 |     const eventsIsDist = eventsPath.includes(path.join(path.sep, "dist", path.sep)) || eventsPath.endsWith(path.join(path.sep, "dist"));
    > 107 |     const eventFiles = readdirSync(eventsPath).filter(file => eventsIsDist ? file.endsWith(".js") : file.endsWith(".ts") || file.endsWith(".js"));
          |                        ^
      108 |     const events: Array<LoadedEventHandler<keyof ClientEvents>> = [];
      109 |
      110 |     logger.debug(`Crawling events in: ${eventsPath}`);

      at readdirSync (bot/src/utility/crawler.ts:107:24)
      at Object.<anonymous> (bot/src/utility/crawler.test.ts:82:30)

  ● crawlEvents › should load events with proper structure

    ENOENT: no such file or directory, scandir '/home/runner/work/Monorepo/Monorepo/src/events'

      105 |     const eventsPath = existsSync(distEventsPath) ? distEventsPath : srcEventsPath;
      106 |     const eventsIsDist = eventsPath.includes(path.join(path.sep, "dist", path.sep)) || eventsPath.endsWith(path.join(path.sep, "dist"));
    > 107 |     const eventFiles = readdirSync(eventsPath).filter(file => eventsIsDist ? file.endsWith(".js") : file.endsWith(".ts") || file.endsWith(".js"));
          |                        ^
      108 |     const events: Array<LoadedEventHandler<keyof ClientEvents>> = [];
      109 |
      110 |     logger.debug(`Crawling events in: ${eventsPath}`);

      at readdirSync (bot/src/utility/crawler.ts:107:24)
      at Object.<anonymous> (bot/src/utility/crawler.test.ts:87:30)

  ● crawlEvents › should load the InteractionCreate event if it exists

    ENOENT: no such file or directory, scandir '/home/runner/work/Monorepo/Monorepo/src/events'

      105 |     const eventsPath = existsSync(distEventsPath) ? distEventsPath : srcEventsPath;
      106 |     const eventsIsDist = eventsPath.includes(path.join(path.sep, "dist", path.sep)) || eventsPath.endsWith(path.join(path.sep, "dist"));
    > 107 |     const eventFiles = readdirSync(eventsPath).filter(file => eventsIsDist ? file.endsWith(".js") : file.endsWith(".ts") || file.endsWith(".js"));
          |                        ^
      108 |     const events: Array<LoadedEventHandler<keyof ClientEvents>> = [];
      109 |
      110 |     logger.debug(`Crawling events in: ${eventsPath}`);

      at readdirSync (bot/src/utility/crawler.ts:107:24)
      at Object.<anonymous> (bot/src/utility/crawler.test.ts:100:30)

This issue seems to only happen when it was ran using the ArtiomTr/jest-coverage-report-action@v2 action.

Copilot stopped work on behalf of AlphaGameDeveloper due to an error November 16, 2025 19:18
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