Skip to content

feat: /profile command with engagement tracking#111

Merged
BillChirico merged 10 commits intomainfrom
feat/profile-command
Feb 27, 2026
Merged

feat: /profile command with engagement tracking#111
BillChirico merged 10 commits intomainfrom
feat/profile-command

Conversation

@BillChirico
Copy link
Collaborator

Summary

Implements the /profile slash command with engagement tracking (#44).

Changes

  • migrations/008_user_stats.cjs — New user_stats table with guild/user PK, tracking messages sent, reactions given/received, days active, first seen, and last active timestamps.
  • src/modules/engagement.jstrackMessage() and trackReaction() exports. Fire-and-forget, upsert via ON CONFLICT DO UPDATE, days_active increments on new calendar day. Gated on config.engagement.enabled.
  • src/commands/profile.js/profile [user] command. Rich embed with all stats, activity badge (🌱 Newcomer / 🌿 Regular / 🌳 Veteran / 👑 Legend), #5865F2 color. Defers then edits reply.
  • src/modules/events.js — Wired trackMessage() into messageCreate and trackReaction() into MessageReactionAdd (both fire-and-forget).
  • config.json — Added engagement config block (enabled: false by default), added profile: "everyone" permission.
  • src/api/utils/configAllowlist.js — Added 'engagement' to SAFE_CONFIG_KEYS.
  • web/src/components/dashboard/config-editor.tsx — Added Engagement Tracking toggle to Community Features card.
  • tests/commands/profile.test.js + tests/modules/engagement.test.js — Full coverage including all badge tiers, disabled config, no guild, no stats, db errors, self vs. other user.

Test Results

  • 90 test files, 1848 tests — all pass ✅
  • Biome lint — 0 errors ✅

Closes #44

Copilot AI review requested due to automatic review settings February 27, 2026 17:58
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 27, 2026

Warning

Rate limit exceeded

@BillChirico has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 1 minutes and 19 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between bb3a11a and 6862b82.

📒 Files selected for processing (5)
  • config.json
  • src/commands/profile.js
  • src/modules/engagement.js
  • tests/commands/profile.test.js
  • web/src/components/dashboard/config-editor.tsx
📝 Walkthrough

Walkthrough

Adds engagement tracking: new DB migration for user_stats, new engagement config section, an engagement module (message/reaction trackers), event wiring to call trackers, a /profile slash command displaying engagement stats and badges, dashboard/editor support, and unit tests for both command and tracking logic.

Changes

Cohort / File(s) Summary
Config
config.json, src/api/utils/configAllowlist.js
Add top-level engagement config and permit it in SAFE_CONFIG_KEYS (fields: enabled, trackMessages, trackReactions).
Dashboard
web/src/components/dashboard/config-editor.tsx
Recognize engagement as a guild config section and expose toggle UI in the Community Features area.
Database Migration
migrations/008_user_stats.cjs
Create user_stats table (PK: guild_id,user_id) with counters (messages_sent, reactions_given, reactions_received, days_active), first_seen/last_active timestamps, and an index on guild_id; drop on down.
Engagement Module
src/modules/engagement.js
New exports trackMessage(message) and trackReaction(reaction, user) that guard on config/guild/bot and upsert/increment user_stats (fire-and-forget with error logging/rethrow).
Event Wiring
src/modules/events.js
Import and invoke trackMessage in messageCreate and trackReaction in reactionAdd handlers as non-blocking calls with .catch to swallow errors.
Profile Command
src/commands/profile.js
New /profile [user] slash command: fetches user_stats, computes activity badge (Newcomer/Regular/Veteran/Legend), formats timestamps and embed, and replies; includes guards for guild context and engagement disabled.
Tests
tests/commands/profile.test.js, tests/modules/engagement.test.js
Add unit tests covering badge logic, /profile scenarios (guards, DB/no-DB, target user, error handling) and engagement tracking behaviors (message/reaction upserts, skips, and error paths).

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: /profile command with engagement tracking' accurately reflects the main change: implementing a profile command with underlying engagement tracking functionality.
Description check ✅ Passed The description is detailed and directly related to the changeset, clearly outlining all modifications across database, modules, commands, configuration, web UI, and tests.
Linked Issues check ✅ Passed All requirements from issue #44 are met: user_stats table created [008_user_stats.cjs], engagement tracking functions implemented [engagement.js], /profile command with badge tiers [profile.js], event listeners wired [events.js], configuration controls added [config.json, configAllowlist.js], and comprehensive tests included.
Out of Scope Changes check ✅ Passed All changes are directly scoped to issue #44 requirements: database schema, engagement tracking, profile command, configuration, UI components, and corresponding tests. No unrelated changes detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/profile-command

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@claude
Copy link

claude bot commented Feb 27, 2026

Claude finished @BillChirico's task in 2m 15s —— View job


Code Review: PR #111/profile command with engagement tracking

  • Read AGENTS.md for project conventions
  • Read all changed files
  • Analyze code for issues
  • Post inline comments
  • Submit review verdict

Verdict: Request Changes — 1 warning, 4 nitpicks.

Previous review findings (dead config flags, bot exclusion, error re-throw, bot guard on reactions_received, days_active off-by-one, last_active passive inflation, Promise.all parallelization) have all been addressed through fixup commits.

Remaining issues:

  • 🟡 Missing AGENTS.md + README.md documentation updates (per project policy)
  • 🔵 Redundant idx_user_stats_guild index (covered by composite PK)
  • 🔵 err.message undefined for non-Error rejections (engagement.js:48, 110)
  • 🔵 Duplicate default badge array x4 in config-editor.tsx

See review comments for details and fix-all prompt.

Copy link

@claude claude bot left a comment

Choose a reason for hiding this comment

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

Review Summary

3 issues found (2 warnings, 2 nitpicks) — requesting changes for the warnings.

🟡 Warnings

  1. Dead config flags trackMessages / trackReactions (config.json:187-188)
    Config defines engagement.trackMessages and engagement.trackReactions but src/modules/engagement.js never reads them — it only checks engagement.enabled. Either wire them up or remove them to avoid misleading operators.

  2. Missing documentation updates (AGENTS.md, README.md)
    Per the project's own docs policy (AGENTS.md lines 170-189), new commands/modules require updates to:

    • AGENTS.md Key Files table — add src/commands/profile.js, src/modules/engagement.js, migrations/008_user_stats.cjs
    • AGENTS.md Database Tables — add user_stats row
    • README.md Features list — add /profile and engagement tracking

🔵 Nitpicks

  1. Sequential DB queries in trackReaction() (src/modules/engagement.js:72-84)
    The reactions_given and reactions_received upserts are independent and could be parallelized with Promise.all().

  2. reactions_received upsert skips days_active/last_active (src/modules/engagement.js:89-95)
    Asymmetry: reactions_given updates activity timestamps but reactions_received does not. If intentional, add a comment explaining why.


📋 Fix-all prompt (copy → paste into AI agent)
Fix the following issues on branch feat/profile-command in VolvoxLLC/volvox-bot:

1. config.json lines 187-188: Remove the unused `trackMessages` and `trackReactions` keys
   from the `engagement` config block (keep only `enabled`). Alternatively, wire them up in
   src/modules/engagement.js — check `config.engagement.trackMessages` before calling the
   message upsert and `config.engagement.trackReactions` before the reaction upsert.

2. AGENTS.md — Add to the Key Files table:
   | `src/commands/profile.js` | `/profile` command — shows user engagement stats (messages, reactions, days active, activity badge) |
   | `src/modules/engagement.js` | Engagement tracking — fire-and-forget upserts for message/reaction activity |
   | `migrations/008_user_stats.cjs` | Migration — creates `user_stats` table (guild_id, user_id, messages_sent, reactions_given, reactions_received, days_active, first_seen, last_active) |

   Add to the Database Tables section:
   | `user_stats` | Per-guild user engagement stats — messages sent, reactions given/received, days active, first/last seen timestamps |

3. README.md — Add a bullet to the Features list:
   - **📊 Engagement Profiles** — `/profile` command showing messages sent, reactions given/received, days active, and activity badges.

4. src/modules/engagement.js lines 72-95 (trackReaction function): Parallelize the two
   independent DB queries using Promise.all() instead of sequential awaits.

5. src/modules/engagement.js line 89-95: Add a code comment above the reactions_received
   query explaining that days_active/last_active are intentionally not updated here because
   receiving reactions is passive activity.

@greptile-apps
Copy link

greptile-apps bot commented Feb 27, 2026

Greptile Summary

Implements /profile command with comprehensive engagement tracking for messages, reactions, and activity streaks. The implementation is well-architected with proper separation of concerns, fire-and-forget patterns for non-blocking tracking, and extensive test coverage (411 lines across 2 test files).

Key Strengths:

  • Correct days_active logic with off-by-one guards for rows seeded by passive engagement
  • Bot filtering prevents bots from inflating both active and passive stats
  • Config flags (trackMessages, trackReactions) are properly respected
  • Fire-and-forget pattern prevents blocking the main event loop
  • Configurable activity badges via dashboard with add/edit/delete controls
  • Comprehensive test coverage including edge cases and error scenarios

Issue Found:

  • Migration sets last_active TIMESTAMPTZ DEFAULT NOW() which incorrectly timestamps users who only receive reactions (passive engagement). This causes /profile to show a "Last Active" date for users who have never actually been active. Fix: change to DEFAULT NULL so these users display "Never" as intended.

Confidence Score: 4/5

  • Safe to merge after fixing the last_active DEFAULT in the migration
  • Solid implementation with one logical issue: the migration's DEFAULT NOW() for last_active incorrectly timestamps passive engagement. The core tracking logic, bot filtering, config checks, and days_active calculation are all correct. Excellent test coverage (90 test files, 1848 tests passing).
  • migrations/008_user_stats.cjs — change last_active DEFAULT to NULL

Important Files Changed

Filename Overview
migrations/008_user_stats.cjs Creates user_stats table with proper indexes. Minor issue: last_active DEFAULT sets timestamp for passive engagement.
src/modules/engagement.js Solid tracking logic with proper bot filtering, config checks, and days_active calculation. Correctly handles passive vs active engagement.
src/commands/profile.js Well-implemented command with proper error handling, config validation, and flexible badge tiers. Handles missing data gracefully.
src/modules/events.js Fire-and-forget integration for both message and reaction tracking. Properly handles partial messages and uses non-blocking pattern.

Sequence Diagram

sequenceDiagram
    participant User
    participant Discord
    participant Events
    participant Engagement
    participant DB
    participant Profile

    User->>Discord: Send message
    Discord->>Events: messageCreate event
    Events->>Engagement: trackMessage() [fire-and-forget]
    Engagement->>DB: UPSERT user_stats<br/>(messages_sent++, days_active logic)
    
    User->>Discord: Add reaction
    Discord->>Events: MessageReactionAdd event
    Events->>Engagement: trackReaction() [fire-and-forget]
    Engagement->>DB: UPSERT reactor stats<br/>(reactions_given++)
    Engagement->>DB: UPSERT author stats<br/>(reactions_received++)
    
    User->>Discord: /profile command
    Discord->>Profile: execute()
    Profile->>DB: SELECT user_stats
    DB-->>Profile: stats data
    Profile->>Profile: Calculate badge tier
    Profile-->>Discord: Embed with stats
    Discord-->>User: Display profile
Loading

Last reviewed commit: 6862b82

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

9 files reviewed, 7 comments

Edit Code Review Agent Settings | Greptile

Copy link

@claude claude bot left a comment

Choose a reason for hiding this comment

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

2 warnings, 1 nitpick found. See inline comments for details.

🟡 Warnings:

  1. Inconsistent last_active in reactions_received upsert (src/modules/engagement.js:91-97) — last_active is in the INSERT but not the ON CONFLICT UPDATE, freezing it at first-reaction time. Remove it from INSERT or add to UPDATE.
  2. Missing documentation updates — Per AGENTS.md §Documentation policy, new commands/modules/tables require updates to AGENTS.md (Key Files, Database Tables) and README.md (Features list).

🔵 Nitpick:
3. Sequential DB queries in trackReaction() (src/modules/engagement.js:74-97) — The two independent upserts could be parallelized with Promise.all().

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/modules/engagement.js`:
- Around line 43-49: The catch block in src/modules/engagement.js logs tracking
errors but swallows them; update the catch in the function that wraps message
tracking (e.g., the function handling message engagement /
trackMessageEngagement) to re-throw the error after logging, wrapping it in the
appropriate custom error class from src/utils/errors.js (use the specific class
provided there, e.g., TrackingError or similar) so upstream callers can observe
failures; apply the same change to the other catch at lines ~99-105 (the other
tracking/error-handling block) — log with logError as currently done, then throw
new CustomError(originalError, { context }) or re-throw the wrapped error.
- Around line 20-25: Summary: Engagement counters are including bot/webhook
activity; exclude automated accounts so days_active and badge tiers reflect
humans only. Fix: in the message handler (around the initial guard where
getConfig is called) add a guard to return if message.author?.bot or
message.webhookId is set before calling trackMessage; likewise in the reaction
handler (the function handling reactions where trackReaction is called,
referenced around lines 62-68) return/ignore if the reacting user?.bot or the
reaction is from a webhook; also ensure trackMessage and trackReaction
themselves defensively check and return early when given a bot/webhook user. Use
the symbols message.author?.bot, message.webhookId, and the reaction handler’s
user?.bot to locate and implement these checks.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4fe7227 and 0ebdb99.

📒 Files selected for processing (9)
  • config.json
  • migrations/008_user_stats.cjs
  • src/api/utils/configAllowlist.js
  • src/commands/profile.js
  • src/modules/engagement.js
  • src/modules/events.js
  • tests/commands/profile.test.js
  • tests/modules/engagement.test.js
  • web/src/components/dashboard/config-editor.tsx
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Greptile Review
  • GitHub Check: claude-review
🧰 Additional context used
📓 Path-based instructions (7)
**/*.js

📄 CodeRabbit inference engine (AGENTS.md)

**/*.js: Use ESM modules only — import/export, never require()
Always use node: protocol for Node.js builtin imports (e.g., import { readFileSync } from 'node:fs')

Files:

  • tests/commands/profile.test.js
  • src/modules/engagement.js
  • src/modules/events.js
  • src/commands/profile.js
  • src/api/utils/configAllowlist.js
  • tests/modules/engagement.test.js
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{js,ts,tsx}: Always use semicolons
Use single quotes — enforced by Biome
Use 2-space indentation — enforced by Biome

Files:

  • tests/commands/profile.test.js
  • src/modules/engagement.js
  • src/modules/events.js
  • src/commands/profile.js
  • web/src/components/dashboard/config-editor.tsx
  • src/api/utils/configAllowlist.js
  • tests/modules/engagement.test.js
src/**/*.js

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.js: NEVER use console.log, console.warn, console.error, or any console.* method in src/ files — always use Winston logger instead: import { info, warn, error } from '../logger.js'
Pass structured metadata to Winston logger: info('Message processed', { userId, channelId })
Use custom error classes from src/utils/errors.js and always log errors with context before re-throwing
Use getConfig(guildId?) from src/modules/config.js to read config; use setConfigValue(path, value, guildId?) to update at runtime
Use safeSend() utility for outgoing Discord messages to sanitize mentions and enforce allowedMentions
Use splitMessage() utility for messages exceeding Discord's 2000-character limit
Add tests for all new code with mandatory 80% coverage threshold on statements, branches, functions, and lines; run pnpm test before every commit

Files:

  • src/modules/engagement.js
  • src/modules/events.js
  • src/commands/profile.js
  • src/api/utils/configAllowlist.js
src/modules/*.js

📄 CodeRabbit inference engine (AGENTS.md)

src/modules/*.js: Register event handlers in src/modules/events.js by importing handler functions and calling client.on() with config parameter
Check config.yourModule.enabled before processing in module event handlers
Prefer per-request getConfig() pattern in new modules over reactive onConfigChange() wiring; only add onConfigChange() listeners for stateful resources that cannot re-read config on each use

Files:

  • src/modules/engagement.js
  • src/modules/events.js
src/commands/*.js

📄 CodeRabbit inference engine (AGENTS.md)

src/commands/*.js: Use parseDuration() from src/utils/duration.js for duration-based commands (timeout, tempban, slowmode); enforce Discord duration caps (timeouts max 28 days, slowmode max 6 hours)
Create slash commands by exporting data (SlashCommandBuilder) and execute() function from src/commands/*.js; export adminOnly = true for mod-only commands; commands are auto-discovered on startup

Files:

  • src/commands/profile.js
web/**/*.{tsx,ts}

📄 CodeRabbit inference engine (AGENTS.md)

Use next/image Image component with appropriate layout and sizing props in Next.js components

Files:

  • web/src/components/dashboard/config-editor.tsx
web/src/**/*.{tsx,ts}

📄 CodeRabbit inference engine (AGENTS.md)

Use Zustand store (zustand) for state management in React components; implement fetch-on-demand pattern in stores

Files:

  • web/src/components/dashboard/config-editor.tsx
🧠 Learnings (7)
📚 Learning: 2026-02-27T16:24:15.054Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-27T16:24:15.054Z
Learning: Applies to src/**/*.js : Use safeSend() utility for outgoing Discord messages to sanitize mentions and enforce allowedMentions

Applied to files:

  • src/modules/events.js
📚 Learning: 2026-02-27T16:24:15.054Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-27T16:24:15.054Z
Learning: Applies to src/**/*.js : Use splitMessage() utility for messages exceeding Discord's 2000-character limit

Applied to files:

  • src/modules/events.js
📚 Learning: 2026-02-27T16:24:15.054Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-27T16:24:15.054Z
Learning: Applies to src/**/*.js : Use getConfig(guildId?) from src/modules/config.js to read config; use setConfigValue(path, value, guildId?) to update at runtime

Applied to files:

  • src/modules/events.js
  • web/src/components/dashboard/config-editor.tsx
📚 Learning: 2026-02-27T16:24:15.054Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-27T16:24:15.054Z
Learning: Applies to src/commands/*.js : Create slash commands by exporting data (SlashCommandBuilder) and execute() function from src/commands/*.js; export adminOnly = true for mod-only commands; commands are auto-discovered on startup

Applied to files:

  • src/commands/profile.js
📚 Learning: 2026-02-27T16:24:15.054Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-27T16:24:15.054Z
Learning: Applies to src/commands/*.js : Use parseDuration() from src/utils/duration.js for duration-based commands (timeout, tempban, slowmode); enforce Discord duration caps (timeouts max 28 days, slowmode max 6 hours)

Applied to files:

  • src/commands/profile.js
📚 Learning: 2026-02-27T16:24:15.054Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-27T16:24:15.054Z
Learning: Applies to src/commands/*mod*.js : Moderation commands must follow the shared pattern: deferReply({ ephemeral: true }), validate inputs, sendDmNotification(), execute Discord action, createCase(), sendModLogEmbed(), checkEscalation()

Applied to files:

  • src/commands/profile.js
📚 Learning: 2026-02-27T16:24:15.054Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-27T16:24:15.054Z
Learning: Applies to src/**/*.js : Add tests for all new code with mandatory 80% coverage threshold on statements, branches, functions, and lines; run pnpm test before every commit

Applied to files:

  • tests/modules/engagement.test.js
🧬 Code graph analysis (5)
tests/commands/profile.test.js (4)
src/commands/profile.js (5)
  • target (55-55)
  • getActivityBadge (20-25)
  • execute (39-102)
  • pool (54-54)
  • pool (57-62)
src/modules/config.js (1)
  • getConfig (282-313)
src/utils/safeSend.js (1)
  • safeEditReply (178-185)
src/db.js (1)
  • getPool (142-147)
src/modules/engagement.js (3)
src/commands/profile.js (3)
  • config (46-46)
  • pool (54-54)
  • pool (57-62)
src/modules/config.js (2)
  • getConfig (282-313)
  • err (94-94)
src/db.js (1)
  • getPool (142-147)
src/modules/events.js (1)
src/modules/engagement.js (2)
  • trackMessage (19-50)
  • trackReaction (61-106)
src/commands/profile.js (3)
src/utils/safeSend.js (1)
  • safeEditReply (178-185)
src/modules/config.js (2)
  • getConfig (282-313)
  • err (94-94)
src/db.js (1)
  • getPool (142-147)
tests/modules/engagement.test.js (3)
src/modules/engagement.js (6)
  • guildId (62-62)
  • authorId (89-89)
  • pool (27-27)
  • pool (70-70)
  • trackMessage (19-50)
  • trackReaction (61-106)
src/modules/config.js (1)
  • getConfig (282-313)
src/db.js (1)
  • getPool (142-147)
🔇 Additional comments (19)
src/api/utils/configAllowlist.js (1)

23-23: LGTM!

The addition of 'engagement' to SAFE_CONFIG_KEYS correctly enables API access for the new engagement configuration section, aligning with the config.json and dashboard UI changes in this PR.

migrations/008_user_stats.cjs (1)

1-29: LGTM!

The migration correctly creates the user_stats table with:

  • Composite primary key (guild_id, user_id) for per-guild user tracking
  • Appropriate integer counters with defaults
  • Timestamps with TIMESTAMPTZ for timezone awareness
  • An index on guild_id for efficient guild-scoped queries
  • Proper rollback via DROP TABLE ... CASCADE
web/src/components/dashboard/config-editor.tsx (2)

51-51: LGTM!

The engagement key is correctly added to knownSections, enabling the type guard to recognize engagement as a valid guild configuration section.


1205-1205: LGTM!

The engagement tracking toggle is properly integrated into the Community Features card, following the established pattern and providing a clear description of the feature.

config.json (2)

152-153: LGTM!

The profile command permission is correctly set to "everyone", allowing all users to view their own or others' engagement profiles.


185-189: LGTM!

The engagement configuration block is well-structured:

  • enabled: false provides a safe opt-in default
  • trackMessages and trackReactions allow granular control when the feature is enabled
src/modules/events.js (3)

15-15: LGTM!

Correctly imports the engagement tracking functions from the new module.


156-158: LGTM!

Fire-and-forget pattern is correctly implemented. The empty .catch() is acceptable since trackMessage internally logs errors before they propagate.


276-279: LGTM!

Fire-and-forget pattern is correctly implemented for reaction tracking. The placement after guildConfig retrieval ensures the config is available, and the tracking runs independently of the starboard feature gate below.

tests/commands/profile.test.js (3)

1-93: LGTM!

Well-structured test setup with proper mocks for all dependencies and clean helper factories for creating test fixtures.


94-114: LGTM!

Excellent boundary testing for getActivityBadge covering all four tiers with both lower and upper boundary values (0, 6, 7, 29, 30, 89, 90, 200).


116-209: LGTM!

Comprehensive test coverage for the /profile execute function including:

  • Feature disabled/enabled states
  • Guild-only enforcement
  • Zero-stats and populated-stats scenarios
  • Target user lookup
  • Database error handling
  • Self-fallback behavior
tests/modules/engagement.test.js (3)

1-53: LGTM!

Well-organized test setup with proper mocks and helper factories. The mock config correctly includes all three engagement flags (enabled, trackMessages, trackReactions).


55-99: LGTM!

Comprehensive trackMessage tests covering:

  • Successful upsert with correct parameters
  • Feature disabled gate
  • trackMessages flag gate
  • No-guild guard
  • Error handling (logs but doesn't throw)

101-153: LGTM!

Comprehensive trackReaction tests covering:

  • Both reactions_given and reactions_received increments
  • Self-reaction skip logic
  • Feature disabled gate
  • trackReactions flag gate
  • No-guild guard
  • Error handling (logs but doesn't throw)
src/commands/profile.js (3)

1-12: LGTM!

Imports are correctly structured using ESM, the Winston logger is used properly (no console.*), and all required utilities are imported.


14-32: LGTM!

The getActivityBadge function correctly implements the tier thresholds from the requirements (Newcomer <7, Regular 7-29, Veteran 30-89, Legend 90+). The slash command data is properly configured with an optional user option.


39-102: LGTM!

The execute function is well-implemented:

  • Properly defers reply for the potentially slow DB query
  • Validates guild context before proceeding
  • Checks engagement feature flag
  • Handles missing user stats gracefully with defaults
  • Uses safeEditReply for all responses as per guidelines
  • Properly logs errors with context before returning user-friendly message
src/modules/engagement.js (1)

22-25: Good config gating before DB work.

Early returns on engagement.enabled and per-signal flags keep this module lightweight and aligned with the intended runtime behavior.

Also applies to: 65-67

… re-throw on db error

- Add bot guard in trackMessage (message.author?.bot) and trackReaction (user.bot)
  to prevent automated accounts from skewing engagement stats
- Remove last_active from reactions_received INSERT/ON CONFLICT — receiving a
  reaction is passive and should not update the actor's last_active timestamp
- Re-throw DB errors after logging so upstream callers can observe failures;
  update tests to assert rejects instead of resolves
Copy link

@claude claude bot left a comment

Choose a reason for hiding this comment

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

3 issues found (1 warning, 2 nitpicks). The previous review findings (dead config flags, bot exclusion, last_active inconsistency, error swallowing) have all been addressed — nice work on the fixup commits.

Remaining issues:

🟡 Missing documentation updates — Per AGENTS.md §Documentation policy (lines 170-189), new commands/modules/tables require updates to:

  • AGENTS.md Key Files table: add src/commands/profile.js, src/modules/engagement.js, migrations/008_user_stats.cjs
  • AGENTS.md Database Tables: add user_stats row
  • README.md Features list: add engagement tracking / /profile

🟡 Missing test coverage for bot guard (tests/modules/engagement.test.js) — The bot-filtering guards added in d05a614 (message.author?.bot, user.bot) have no test cases. See inline comment for suggested tests.

🔵 Sequential DB queries in trackReaction() (src/modules/engagement.js:73-101) — The two independent upserts could be parallelized with Promise.all(). See inline comment.

…, bot tests

- Skip reactions_received tracking when message author is a bot
- Add last_active to reactions_received upsert (INSERT + ON CONFLICT)
- Add test coverage for bot guard in trackMessage and trackReaction
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copy link

@claude claude bot left a comment

Choose a reason for hiding this comment

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

2 warnings, 1 nitpick

🟡 Warnings:

  1. last_active inflated by passive reactions_received (src/modules/engagement.js:95-102) — When user B reacts to user A's message, A's last_active is set to NOW() even if A is offline. This makes the "Last Active" field on /profile misleading. Remove last_active from both INSERT and UPDATE in the reactions_received upsert.

  2. Missing documentation updates — Per AGENTS.md §Documentation policy (lines 170-189), new commands/modules/tables require updates to:

    • AGENTS.md Key Files table: add src/commands/profile.js, src/modules/engagement.js, migrations/008_user_stats.cjs
    • AGENTS.md Database Tables: add user_stats row
    • README.md Features list: add engagement tracking / /profile

🔵 Nitpick:
3. Sequential DB queries in trackReaction() (src/modules/engagement.js:77-103) — The two independent upserts could be parallelized with Promise.all() to halve round-trip time.


📋 Fix-all prompt (copy → paste into AI agent)
Fix the following issues on branch feat/profile-command in VolvoxLLC/volvox-bot:

1. src/modules/engagement.js lines 95-102: Remove `last_active` from the
   `reactions_received` INSERT and ON CONFLICT UPDATE. Passive engagement
   (receiving reactions) should NOT update `last_active` — only the user's
   own actions should. Change the query to:
     INSERT INTO user_stats (guild_id, user_id, reactions_received, first_seen)
     VALUES ($1, $2, 1, NOW())
     ON CONFLICT (guild_id, user_id) DO UPDATE
       SET reactions_received = user_stats.reactions_received + 1

2. src/modules/engagement.js lines 77-103: Parallelize the two independent
   DB queries (reactions_given and reactions_received) using Promise.all()
   instead of sequential awaits.

3. AGENTS.md — Add to the Key Files table:
   | `src/commands/profile.js` | `/profile` command — shows user engagement stats |
   | `src/modules/engagement.js` | Engagement tracking — fire-and-forget upserts for message/reaction activity |
   | `migrations/008_user_stats.cjs` | Migration — creates `user_stats` table |

   Add to the Database Tables section:
   | `user_stats` | Per-guild user engagement stats — messages sent, reactions given/received, days active, first/last seen |

4. README.md — Add a bullet to the Features list:
   - **📊 Engagement Profiles** — `/profile` command showing messages sent, reactions, days active, and activity badges.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/modules/engagement.js (1)

44-50: ⚠️ Potential issue | 🟠 Major

Wrap and re-throw with the project’s custom error classes.

On Line 50 and Line 110, the code re-throws raw err. In src/**/*.js, these should be wrapped with the appropriate class from src/utils/errors.js after logging context so upstream handling stays consistent.

As per coding guidelines, "Use custom error classes from src/utils/errors.js and always log errors with context before re-throwing".

Also applies to: 104-110

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/engagement.js` around lines 44 - 50, Replace raw re-throws in the
catch blocks that currently call logError(...) and then throw err with wrapping
the original error in the project’s custom error class (e.g., ApplicationError)
from errors.js: after logging via logError('Failed to track message engagement',
{ ... }), create and throw new ApplicationError('Failed to track message
engagement', { cause: err, context: { userId: message.author.id, guildId:
message.guild.id } }); do the same for the other catch (lines around 104–110) so
upstream code receives the ApplicationError while preserving the original error
as the cause.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/modules/engagement.js`:
- Around line 36-40: The days_active increment logic uses last_active::date
which causes a reaction_received update earlier in the same day to block a
subsequent real activity from incrementing days_active; update the CASE
condition to compare day-truncations of timestamps (e.g. date_trunc('day',
$3::timestamptz) > date_trunc('day', user_stats.last_active)) instead of
last_active::date, and when updating last_active in the reactions_received/other
update paths ensure you either set last_active =
GREATEST(user_stats.last_active, $new_ts) or only assign last_active when
$new_ts > user_stats.last_active so later-in-day active events still trigger the
days_active increment (apply same change to the blocks referencing days_active
and last_active).

---

Duplicate comments:
In `@src/modules/engagement.js`:
- Around line 44-50: Replace raw re-throws in the catch blocks that currently
call logError(...) and then throw err with wrapping the original error in the
project’s custom error class (e.g., ApplicationError) from errors.js: after
logging via logError('Failed to track message engagement', { ... }), create and
throw new ApplicationError('Failed to track message engagement', { cause: err,
context: { userId: message.author.id, guildId: message.guild.id } }); do the
same for the other catch (lines around 104–110) so upstream code receives the
ApplicationError while preserving the original error as the cause.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0ebdb99 and 83731bd.

📒 Files selected for processing (2)
  • src/modules/engagement.js
  • tests/modules/engagement.test.js
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Greptile Review
  • GitHub Check: claude-review
🧰 Additional context used
📓 Path-based instructions (4)
**/*.js

📄 CodeRabbit inference engine (AGENTS.md)

**/*.js: Use ESM modules only — import/export, never require()
Always use node: protocol for Node.js builtin imports (e.g., import { readFileSync } from 'node:fs')

Files:

  • src/modules/engagement.js
  • tests/modules/engagement.test.js
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{js,ts,tsx}: Always use semicolons
Use single quotes — enforced by Biome
Use 2-space indentation — enforced by Biome

Files:

  • src/modules/engagement.js
  • tests/modules/engagement.test.js
src/**/*.js

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.js: NEVER use console.log, console.warn, console.error, or any console.* method in src/ files — always use Winston logger instead: import { info, warn, error } from '../logger.js'
Pass structured metadata to Winston logger: info('Message processed', { userId, channelId })
Use custom error classes from src/utils/errors.js and always log errors with context before re-throwing
Use getConfig(guildId?) from src/modules/config.js to read config; use setConfigValue(path, value, guildId?) to update at runtime
Use safeSend() utility for outgoing Discord messages to sanitize mentions and enforce allowedMentions
Use splitMessage() utility for messages exceeding Discord's 2000-character limit
Add tests for all new code with mandatory 80% coverage threshold on statements, branches, functions, and lines; run pnpm test before every commit

Files:

  • src/modules/engagement.js
src/modules/*.js

📄 CodeRabbit inference engine (AGENTS.md)

src/modules/*.js: Register event handlers in src/modules/events.js by importing handler functions and calling client.on() with config parameter
Check config.yourModule.enabled before processing in module event handlers
Prefer per-request getConfig() pattern in new modules over reactive onConfigChange() wiring; only add onConfigChange() listeners for stateful resources that cannot re-read config on each use

Files:

  • src/modules/engagement.js
🧠 Learnings (3)
📚 Learning: 2026-02-27T16:24:15.054Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-27T16:24:15.054Z
Learning: Applies to src/**/*.js : Use custom error classes from src/utils/errors.js and always log errors with context before re-throwing

Applied to files:

  • src/modules/engagement.js
📚 Learning: 2026-02-27T16:24:15.054Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-27T16:24:15.054Z
Learning: Applies to src/modules/moderation.js : Case numbering must be per-guild sequential, assigned atomically using COALESCE(MAX(case_number), 0) + 1 in a single INSERT statement

Applied to files:

  • src/modules/engagement.js
📚 Learning: 2026-02-27T16:24:15.054Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-27T16:24:15.054Z
Learning: Applies to src/**/*.js : Add tests for all new code with mandatory 80% coverage threshold on statements, branches, functions, and lines; run pnpm test before every commit

Applied to files:

  • tests/modules/engagement.test.js
🧬 Code graph analysis (2)
src/modules/engagement.js (3)
src/commands/profile.js (3)
  • config (46-46)
  • pool (54-54)
  • pool (57-62)
src/modules/config.js (2)
  • getConfig (282-313)
  • err (94-94)
src/db.js (1)
  • getPool (142-147)
tests/modules/engagement.test.js (1)
src/modules/engagement.js (6)
  • guildId (64-64)
  • authorId (93-93)
  • pool (28-28)
  • pool (73-73)
  • trackMessage (19-52)
  • trackReaction (63-112)

Updating last_active when a user passively receives a reaction inflates
their activity timestamp — even if they haven't been online in weeks.
It also suppresses days_active increments: if a reaction sets last_active
to today, a subsequent message today can't increment days_active.

Remove last_active from the reactions_received ON CONFLICT UPDATE clause.
The INSERT path retains last_active=NOW() for first-time record creation,
which is harmless (no prior record to corrupt).

Resolves PR review threads:
- last_active inflation from passive receives
- days_active suppression via premature last_active update
- inconsistent last_active in upsert UPDATE clause
coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 27, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link

@claude claude bot left a comment

Choose a reason for hiding this comment

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

1 warning, 2 nitpicks

The code itself is solid after 8 fixup commits — bot guards, config flag gating, days_active off-by-one fix, Promise.all parallelization, and last_active passive-inflation fix are all correctly implemented. Tests cover all branches including bot guards.

🟡 Missing documentation updates (AGENTS.md, README.md)

Per AGENTS.md §Documentation policy (lines 170-189): "Added a new command → update Key Files table, add to README command list" and "Added a new module → update Key Files table, document config section."

Missing updates:

  • AGENTS.md Key Files table: add src/commands/profile.js, src/modules/engagement.js, migrations/008_user_stats.cjs
  • AGENTS.md Database Tables: add user_stats row
  • README.md Features list (line 19): add engagement tracking / /profile
  • README.md Config reference (after line 227): add engagement config section

🔵 err.message may be undefined for non-Error rejections (src/modules/engagement.js:48, 110)

See inline comments.


📋 Fix-all prompt (copy → paste into AI agent)
Fix the following issues on branch feat/profile-command in VolvoxLLC/volvox-bot:

1. AGENTS.md — Add to the Key Files table (after the config.json row, around line 68):
   | `src/commands/profile.js` | `/profile` command — shows user engagement stats (messages, reactions, days active, activity badge) |
   | `src/modules/engagement.js` | Engagement tracking — fire-and-forget upserts for message/reaction activity |
   | `migrations/008_user_stats.cjs` | Migration — creates `user_stats` table (guild_id + user_id PK, counters, timestamps) |

   Add to the Database Tables section (after the afk_pings row, around line 143):
   | `user_stats` | Per-guild user engagement stats — messages sent, reactions given/received, days active, first/last seen timestamps |

2. README.md line 19 — Add a bullet to the Features list after the Voice Activity entry:
   - **📊 Engagement Profiles** — `/profile` command showing messages sent, reactions given/received, days active, and activity badges.

3. README.md after line 227 (after the Reputation section) — Add engagement config reference:

   ### Engagement (`engagement`)

   | Key | Type | Description |
   |-----|------|-------------|
   | `enabled` | boolean | Enable engagement tracking and `/profile` command |
   | `trackMessages` | boolean | Track messages sent per user (default: `true`) |
   | `trackReactions` | boolean | Track reactions given/received per user (default: `true`) |

   **Commands:** `/profile [user]` — show engagement stats with activity badge (🌱 Newcomer / 🌿 Regular / 🌳 Veteran / 👑 Legend).

4. src/modules/engagement.js line 48: Change `error: err.message` to
   `error: err instanceof Error ? err.message : String(err)`

5. src/modules/engagement.js line 110: Same change as #4.

Copy link

@claude claude bot left a comment

Choose a reason for hiding this comment

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

1 warning, 4 nitpicks

Previous review findings (dead config flags, bot exclusion, error re-throw, bot guard on reactions_received, days_active off-by-one, last_active passive inflation, Promise.all parallelization) have all been addressed through 8 fixup commits — nice work on the iteration.

🟡 Warning:

  1. Missing documentation updates — Per AGENTS.md §Documentation policy (lines 170-189), new commands/modules/tables require:
    • AGENTS.md Key Files table: add src/commands/profile.js, src/modules/engagement.js, migrations/008_user_stats.cjs
    • AGENTS.md Database Tables: add user_stats row
    • README.md Features list (line 19): add engagement tracking / /profile

🔵 Nitpicks:
2. Redundant database index (migrations/008_user_stats.cjs:23) — idx_user_stats_guild is covered by the composite PK's leftmost prefix
3. err.message undefined for non-Error rejections (src/modules/engagement.js:48, 110)
4. Duplicate default badge arrays (web/src/components/dashboard/config-editor.tsx:1233-1267) — Same 4-element fallback array repeated 4 times


📋 Fix-all prompt (copy → paste into AI agent)
Fix the following issues on branch feat/profile-command in VolvoxLLC/volvox-bot:

1. AGENTS.md — Add to the Key Files table (after the config.json row, around line 68):
   | `src/commands/profile.js` | `/profile` command — shows user engagement stats (messages, reactions, days active, activity badge) |
   | `src/modules/engagement.js` | Engagement tracking — fire-and-forget upserts for message/reaction activity |
   | `migrations/008_user_stats.cjs` | Migration — creates `user_stats` table (guild_id + user_id PK, counters, timestamps) |

   Add to the Database Tables section (after the afk_pings row, around line 143):
   | `user_stats` | Per-guild user engagement stats — messages sent, reactions given/received, days active, first/last seen timestamps |

2. README.md line 19 — Add a bullet to the Features list after the Voice Activity entry:
   - **📊 Engagement Profiles** — `/profile` command showing messages sent, reactions given/received, days active, and activity badges.

3. migrations/008_user_stats.cjs line 23 — Remove the redundant index:
   Delete the line: pgm.sql('CREATE INDEX IF NOT EXISTS idx_user_stats_guild ON user_stats(guild_id)');
   The composite PK (guild_id, user_id) already covers guild_id lookups.

4. src/modules/engagement.js lines 48 and 110 — Change `error: err.message` to:
   error: err instanceof Error ? err.message : String(err)

5. web/src/components/dashboard/config-editor.tsx — Extract the default badge array
   (repeated at lines ~1233, ~1246, ~1262) into a constant:
   const DEFAULT_ACTIVITY_BADGES = [
     { days: 90, label: "👑 Legend" },
     { days: 30, label: "🌳 Veteran" },
     { days: 7, label: "🌿 Regular" },
     { days: 0, label: "🌱 Newcomer" },
   ];
   Then replace all 4 occurrences of the inline fallback array with DEFAULT_ACTIVITY_BADGES.

@greptile-apps
Copy link

greptile-apps bot commented Feb 27, 2026

Additional Comments (1)

migrations/008_user_stats.cjs, line 18
DEFAULT NOW() sets last_active for users who only receive reactions (passive engagement). Change to DEFAULT NULL so /profile shows "Never" for truly inactive users.

      last_active TIMESTAMPTZ DEFAULT NULL,

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.

/profile command — track and display community engagement stats

2 participants