Skip to content

feat: credits notification#841

Closed
alexey-yarmosh wants to merge 14 commits into
masterfrom
credits-notif
Closed

feat: credits notification#841
alexey-yarmosh wants to merge 14 commits into
masterfrom
credits-notif

Conversation

@alexey-yarmosh
Copy link
Copy Markdown
Member

@alexey-yarmosh alexey-yarmosh commented May 13, 2026

Fixes jsdelivr/globalping-dash#198

Explicit threshold approach is used as the simplest to understand and use.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 13, 2026

Review Change Stack

Walkthrough

This PR implements a low-credits notification feature. The database schema is extended with a notification_preferences JSON column on directus_users and a new directus_settings table storing the global default threshold. The Credits class gains a background sync loop that periodically refreshes per-user preferences from the database and implements threshold-crossing detection: when a user's remaining credits drop from above to at/below their configured threshold, a POST request is sent to the Directus notifications endpoint. Server initialization wires up the sync on startup. Unit tests cover all sync and notification paths, while integration tests verify the complete flow end-to-end. A separate fix ensures test stub lifecycle isolation in adopted-probes tests.

Possibly related PRs

  • jsdelivr/globalping#777: Modifies the same server startup area around auth.syncTokens(), overlapping initialization flow changes.

Suggested reviewers

  • MartinKolarik
🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR implements threshold-based low-credit detection (#198) with notifications sent via Directus, includes configurable default and per-user thresholds, but does not implement email notifications as optionally requested. Consider implementing email notification support or document why email integration (#27) was deferred as out of scope for this PR.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: credits notification' directly and concisely describes the main change—adding credit low-balance notification functionality.
Out of Scope Changes check ✅ Passed All changes align with threshold-based low-credit notifications and supporting infrastructure. The constraint rename in gp_schedule_configuration is minor and supportive.
Description check ✅ Passed The PR description references issue #198 and mentions using an explicit threshold approach, which aligns with the changeset adding threshold-based low-credits notifications.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch credits-notif

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

Copy link
Copy Markdown

@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: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@migrations/dashboard/create-tables.js.sql`:
- Around line 51-54: The low_credits_default_threshold column in
directus_settings allows negative values; update the CREATE TABLE for
directus_settings to enforce non-negative values by adding a CHECK constraint
like CHECK (low_credits_default_threshold >= 0) (or change the column to INT
UNSIGNED NULL) so the column low_credits_default_threshold cannot be negative
while still allowing NULL if desired.

In `@src/lib/credits.ts`:
- Around line 115-120: In refreshDefaultThreshold, the cached
this.defaultThreshold is never cleared when
directus_settings.low_credits_default_threshold is NULL or the row is missing;
update the method (refreshDefaultThreshold) so that if typeof
row?.low_credits_default_threshold !== 'number' you explicitly set
this.defaultThreshold to null (or the intended disabled sentinel) instead of
leaving the previous value, ensuring runtime disabling works when the DB value
is cleared.
- Around line 98-101: The threshold check treats 0 as falsy and skips
notifications; update the conditional in the credits logic so it explicitly
checks for undefined/null rather than truthiness: use the computed threshold
(variable threshold) and change the guard from "if (threshold && previousAmount
> threshold && remainingAmount <= threshold)" to an explicit existence check
(e.g., threshold !== undefined && threshold !== null) so that threshold === 0
will still trigger the notifications path that calls got.post to
`${directusUrl}/notifications` when previousAmount and remainingAmount cross the
threshold.

In `@src/lib/server.ts`:
- Around line 98-101: The Promise.all currently fails startup if
credits.syncPreferences() rejects; change the second element so only
credits.syncPreferences is locally handled: keep using logIfTooLong but wrap the
credits.syncPreferences() invocation in a try/catch or append .catch(...) to log
the error (e.g., processLogger.warn/processLogger.error) and swallow it so it
resolves, while leaving logIfTooLong(auth.syncTokens(), 'auth.syncTokens')
unchanged; reference the credits.syncPreferences and logIfTooLong call sites so
the startup continues even if the credits sync transiently fails.

In `@test/tests/integration/credits-notification.test.ts`:
- Around line 64-80: The test uses a mutable flag (posted) and a fixed sleep
(setTimeout(50)) which is timing-flaky; replace that pattern by capturing the
nock scope returned from
nock('https://dash-directus.globalping.io').post('/notifications', ...) and
after calling requestAgent.post('/v1/measurements') poll the scope with a
bounded wait until scope.isDone() (or call scope.done() in a try/catch with a
short retry loop) to deterministically assert the notification was sent, and for
the negative case assert scope.isDone() === false after the same bounded grace
period; update the assertions around the mutable posted flag and setTimeout(50)
accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 22a13b1a-14bb-4c30-a4ba-651e35508530

📥 Commits

Reviewing files that changed from the base of the PR and between 6507b7b and cb502a3.

📒 Files selected for processing (8)
  • migrations/dashboard/create-tables.js.sql
  • seeds/dashboard/development/index.js
  • seeds/dashboard/test/index.js
  • src/lib/credits.ts
  • src/lib/server.ts
  • test/tests/integration/credits-notification.test.ts
  • test/tests/unit/credits.test.ts
  • test/tests/unit/override/adopted-probes.test.ts

Comment thread migrations/dashboard/create-tables.js.sql
Comment thread src/lib/credits.ts
Comment thread src/lib/credits.ts
Comment thread src/lib/server.ts
Comment thread test/tests/integration/credits-notification.test.ts
Copy link
Copy Markdown

@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.

🧹 Nitpick comments (1)
src/lib/credits.ts (1)

51-52: 💤 Low value

Consider wrapping JSON.parse in try-catch for resilience.

If a single row contains malformed notification_preferences, the entire sync fails and no users are updated. Wrapping the parse in a try-catch and skipping invalid rows would make the sync more robust.

♻️ Suggested improvement
 this.userIdToPreference = new Map(rows.flatMap<[string, false | number]>(({ id, notification_preferences }) => {
-  const prefs = JSON.parse(notification_preferences) as NotificationPreferences;
+  let prefs: NotificationPreferences;
+  try {
+    prefs = JSON.parse(notification_preferences) as NotificationPreferences;
+  } catch {
+    logger.error(`Invalid notification_preferences JSON for user ${id}`);
+    return [];
+  }
   const lowCredits = prefs.low_credits;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/credits.ts` around lines 51 - 52, The current rows.flatMap callback
used to populate userIdToPreference parses notification_preferences with
JSON.parse and can throw, causing the whole sync to fail; wrap the JSON.parse
call inside a try-catch within the flatMap callback (or a small helper used by
it), and on catch skip that row (return [] from the flatMap) and optionally log
the offending id and parse error; ensure the result still conforms to the
expected [string, false | number] tuples for userIdToPreference and keep the
types using NotificationPreferences when parse succeeds.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/lib/credits.ts`:
- Around line 51-52: The current rows.flatMap callback used to populate
userIdToPreference parses notification_preferences with JSON.parse and can
throw, causing the whole sync to fail; wrap the JSON.parse call inside a
try-catch within the flatMap callback (or a small helper used by it), and on
catch skip that row (return [] from the flatMap) and optionally log the
offending id and parse error; ensure the result still conforms to the expected
[string, false | number] tuples for userIdToPreference and keep the types using
NotificationPreferences when parse succeeds.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1dcc05ee-b2e4-462f-a942-4433070b78a5

📥 Commits

Reviewing files that changed from the base of the PR and between cb502a3 and 403a90d.

📒 Files selected for processing (2)
  • migrations/dashboard/create-tables.js.sql
  • src/lib/credits.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • migrations/dashboard/create-tables.js.sql

Copy link
Copy Markdown
Member

@MartinKolarik MartinKolarik left a comment

Choose a reason for hiding this comment

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

I don't think the API should be involved in this at all, especially as we might need to refactor the credits handling soon to introduce caching. Directus can do a simple SELECT * FROM users join credits where credits < threshold every few minutes, and we can add a flag on the user, e.g., creditsBelowThreshold, which decides whether a new notification is sent (not if already true). The flag resets on credit additions if the credits go above the threshold.

Base automatically changed from handshake-validation to deps May 15, 2026 22:08
@MartinKolarik MartinKolarik force-pushed the deps branch 2 times, most recently from 35b42dd to 5e327ba Compare May 15, 2026 22:09
Base automatically changed from deps to master May 15, 2026 22:20
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.

Credits notification

3 participants