Skip to content

Conversation

@danielraffel
Copy link

@danielraffel danielraffel commented Oct 25, 2025

Context

Part 3 of 4-PR series to extract email functionality into adapters (#22771).

Series: #25250#25251PR3#25253

Changes

Creates EmailAnalyticsBase adapter and implements Mailgun analytics provider using the adapter pattern.

The adapter handles fetching email events (delivered, opened, failed, unsubscribed, complained) from Mailgun's API with support for filtering, pagination, and batch processing.

Removes legacy analytics provider in favor of adapter implementation.

Next: PR4 completes the series with email suppression adapter.

ref TryGhost#22771

This establishes the foundation for multi-provider email support using
Ghost's existing AdapterManager pattern. The EmailProviderBase class
defines the contract that all email provider adapters must follow,
enabling community-developed providers to be published as npm packages.

- Created EmailProviderBase with required send() method
- Registered email adapter type in AdapterManager
- Added comprehensive tests validating base class contract
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 25, 2025

Warning

Rate limit exceeded

@danielraffel has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 13 minutes and 44 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 a02259f and 02da44f.

📒 Files selected for processing (17)
  • ghost/core/core/server/adapters/email-analytics/EmailAnalyticsBase.js (1 hunks)
  • ghost/core/core/server/adapters/email-analytics/mailgun/index.js (1 hunks)
  • ghost/core/core/server/adapters/email/EmailProviderBase.js (1 hunks)
  • ghost/core/core/server/adapters/email/mailgun/index.js (6 hunks)
  • ghost/core/core/server/services/adapter-manager/AdapterManager.js (1 hunks)
  • ghost/core/core/server/services/adapter-manager/config.js (1 hunks)
  • ghost/core/core/server/services/adapter-manager/index.js (1 hunks)
  • ghost/core/core/server/services/email-analytics/EmailAnalyticsProviderMailgun.js (0 hunks)
  • ghost/core/core/server/services/email-analytics/EmailAnalyticsServiceWrapper.js (2 hunks)
  • ghost/core/core/server/services/email-service/EmailServiceWrapper.js (2 hunks)
  • ghost/core/test/unit/server/adapters/email-analytics/EmailAnalyticsBase.test.js (1 hunks)
  • ghost/core/test/unit/server/adapters/email-analytics/mailgun/index.test.js (1 hunks)
  • ghost/core/test/unit/server/adapters/email/EmailProviderBase.test.js (1 hunks)
  • ghost/core/test/unit/server/adapters/email/mailgun/index.test.js (1 hunks)
  • ghost/core/test/unit/server/services/adapter-manager/AdapterManager.test.js (1 hunks)
  • ghost/core/test/unit/server/services/email-analytics/EmailAnalyticsProviderMailgun.test.js (0 hunks)
  • ghost/core/test/unit/server/services/email-service/mailgun-email-provider.test.js (0 hunks)

Walkthrough

Adds an adapter-based email analytics system: a new EmailAnalyticsBase class defines the required fetchLatest() contract; a MailgunEmailAnalyticsProvider adapter implements that contract in a new adapters path; the legacy EmailAnalyticsProviderMailgun implementation and its tests were removed. Adapter manager now registers an email-analytics adapter, accepts an optional runtimeConfig merged with file config, and a default adapterServiceConfig entry for email-analytics was introduced. EmailAnalyticsServiceWrapper retrieves the provider via the adapter manager. Unit tests for the base and new mailgun adapter were added.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Verify adapter manager merging and precedence of runtimeConfig with adapterConfig.
  • Inspect EmailAnalyticsBase requiredFns enforcement and the thrown IncorrectUsageError behavior.
  • Review MailgunEmailAnalyticsProvider for parity with removed implementation: event filters, tag handling, begin/end timestamp conversion, PAGE_LIMIT, and batchHandler propagation.
  • Confirm defaulting added in adapter manager config (email-analytics) is correct and won't conflict with runtime injection.
  • Check tests: new base and adapter unit tests, and removal of legacy provider tests for coverage gaps.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The pull request title "Added email analytics adapter support with Mailgun implementation" directly and accurately summarizes the main change in the changeset. The title captures the key objectives: introducing a new EmailAnalyticsBase adapter class and implementing a Mailgun provider using the adapter pattern. The title is concise, specific, and avoids vague terminology—a developer scanning the repository history would immediately understand that this PR adds adapter-based email analytics functionality.
Description Check ✅ Passed The pull request description is directly related to the changeset. It accurately describes the key changes: creation of EmailAnalyticsBase adapter, implementation of the Mailgun analytics provider, the adapter pattern approach, email event handling capabilities, and removal of the legacy provider. The description provides meaningful context by positioning this as part of a broader series and mentions specific file/class names that match the changes in the raw summary.

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.

- Moved require('@tryghost/errors') to top of EmailProviderBase
- Removed duplicate @returns JSDoc tag
- Updated AdapterManager JSDoc to include 'email' adapter type
- Renamed test suite from 'integration with AdapterManager' to 'module exports'
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

🧹 Nitpick comments (1)
ghost/core/test/unit/server/adapters/email-analytics/mailgun/index.test.js (1)

217-224: Consider adding test for undefined options.

While the test at lines 217-224 covers empty options object {}, consider adding a test case for when options is undefined to fully verify the optional parameter contract.

Add this test case:

it('should work when options is undefined', async function () {
    await adapter.fetchLatest(batchHandler);

    const mailgunOptions = mailgunClient.fetchEvents.firstCall.args[0];

    mailgunOptions.event.should.equal('delivered OR opened OR failed OR unsubscribed OR complained');
    mailgunOptions.tags.should.equal('bulk-email');
    should.not.exist(mailgunOptions.begin);
    should.not.exist(mailgunOptions.end);
    
    const options = mailgunClient.fetchEvents.firstCall.args[2];
    should.not.exist(options.maxEvents);
});
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1bf0399 and 1148d5b.

📒 Files selected for processing (9)
  • ghost/core/core/server/adapters/email-analytics/EmailAnalyticsBase.js (1 hunks)
  • ghost/core/core/server/adapters/email-analytics/mailgun/index.js (1 hunks)
  • ghost/core/core/server/services/adapter-manager/config.js (1 hunks)
  • ghost/core/core/server/services/adapter-manager/index.js (1 hunks)
  • ghost/core/core/server/services/email-analytics/EmailAnalyticsProviderMailgun.js (0 hunks)
  • ghost/core/core/server/services/email-analytics/EmailAnalyticsServiceWrapper.js (2 hunks)
  • ghost/core/test/unit/server/adapters/email-analytics/EmailAnalyticsBase.test.js (1 hunks)
  • ghost/core/test/unit/server/adapters/email-analytics/mailgun/index.test.js (1 hunks)
  • ghost/core/test/unit/server/services/email-analytics/EmailAnalyticsProviderMailgun.test.js (0 hunks)
💤 Files with no reviewable changes (2)
  • ghost/core/core/server/services/email-analytics/EmailAnalyticsProviderMailgun.js
  • ghost/core/test/unit/server/services/email-analytics/EmailAnalyticsProviderMailgun.test.js
🧰 Additional context used
📓 Path-based instructions (2)
ghost/core/core/server/**

📄 CodeRabbit inference engine (AGENTS.md)

Backend core logic should reside under ghost/core/core/server/

Files:

  • ghost/core/core/server/services/adapter-manager/config.js
  • ghost/core/core/server/services/email-analytics/EmailAnalyticsServiceWrapper.js
  • ghost/core/core/server/adapters/email-analytics/EmailAnalyticsBase.js
  • ghost/core/core/server/adapters/email-analytics/mailgun/index.js
  • ghost/core/core/server/services/adapter-manager/index.js
ghost/core/core/server/services/**

📄 CodeRabbit inference engine (AGENTS.md)

Backend services should be implemented under ghost/core/core/server/services/

Files:

  • ghost/core/core/server/services/adapter-manager/config.js
  • ghost/core/core/server/services/email-analytics/EmailAnalyticsServiceWrapper.js
  • ghost/core/core/server/services/adapter-manager/index.js
🧠 Learnings (1)
📚 Learning: 2025-10-06T14:19:50.385Z
Learnt from: sam-lord
PR: TryGhost/Ghost#24966
File: ghost/core/test/unit/server/services/mail/GhostMailer.test.js:330-421
Timestamp: 2025-10-06T14:19:50.385Z
Learning: The GhostMailer class in `ghost/core/core/server/services/mail/GhostMailer.js` does not accept external Mailgun-specific parameters (like `o:tag`) from callers. Tags are generated internally via the `getTags()` method. This design keeps the class generic and avoids mixing transport-specific concerns into the public interface.

Applied to files:

  • ghost/core/core/server/adapters/email-analytics/mailgun/index.js
🧬 Code graph analysis (7)
ghost/core/test/unit/server/adapters/email-analytics/EmailAnalyticsBase.test.js (2)
ghost/core/test/unit/server/adapters/email-analytics/mailgun/index.test.js (2)
  • should (1-1)
  • EmailAnalyticsBase (4-4)
ghost/core/core/server/adapters/email-analytics/mailgun/index.js (1)
  • EmailAnalyticsBase (3-3)
ghost/core/core/server/services/adapter-manager/config.js (1)
ghost/core/core/server/services/adapter-manager/index.js (1)
  • adapterServiceConfig (29-29)
ghost/core/core/server/services/email-analytics/EmailAnalyticsServiceWrapper.js (1)
ghost/core/core/server/services/adapter-manager/index.js (1)
  • adapterManager (6-13)
ghost/core/core/server/adapters/email-analytics/EmailAnalyticsBase.js (3)
ghost/core/core/server/adapters/email-analytics/mailgun/index.js (1)
  • EmailAnalyticsBase (3-3)
ghost/core/test/unit/server/adapters/email-analytics/EmailAnalyticsBase.test.js (1)
  • EmailAnalyticsBase (2-2)
ghost/core/test/unit/server/adapters/email-analytics/mailgun/index.test.js (1)
  • EmailAnalyticsBase (4-4)
ghost/core/test/unit/server/adapters/email-analytics/mailgun/index.test.js (2)
ghost/core/test/unit/server/adapters/email-analytics/EmailAnalyticsBase.test.js (1)
  • EmailAnalyticsBase (2-2)
ghost/core/core/server/adapters/email-analytics/mailgun/index.js (1)
  • EmailAnalyticsBase (3-3)
ghost/core/core/server/adapters/email-analytics/mailgun/index.js (2)
ghost/core/core/server/adapters/email-analytics/EmailAnalyticsBase.js (1)
  • errors (1-1)
ghost/core/core/server/services/email-analytics/EmailAnalyticsProviderMailgun.js (1)
  • EmailAnalyticsProviderMailgun (7-60)
ghost/core/core/server/services/adapter-manager/index.js (1)
ghost/core/core/server/services/adapter-manager/config.js (1)
  • adapterServiceConfig (11-11)
🔇 Additional comments (12)
ghost/core/core/server/services/adapter-manager/config.js (1)

28-35: LGTM! Clean default configuration pattern.

The email-analytics adapter default configuration follows the established pattern for storage and scheduling adapters. The empty mailgun object appropriately defers configuration to runtime injection.

ghost/core/core/server/services/email-analytics/EmailAnalyticsServiceWrapper.js (1)

46-58: Clean adapter-manager integration.

The migration from direct require to adapter-manager based provider retrieval is well-implemented. Runtime config injection maintains the existing dependency injection pattern while enabling future provider extensibility.

ghost/core/test/unit/server/adapters/email-analytics/EmailAnalyticsBase.test.js (1)

1-66: Comprehensive base class test coverage.

The test suite thoroughly validates the EmailAnalyticsBase contract including constructor behavior, config handling, requiredFns enforcement, and error handling for unimplemented methods.

ghost/core/core/server/adapters/email-analytics/mailgun/index.js (3)

25-45: Good constructor validation and initialization.

The constructor properly validates required dependencies and initializes the Mailgun client with appropriate configuration. Tag handling supports both default and custom tags as expected.


86-88: Clean delegation to MailgunClient.

The private wrapper method appropriately delegates to the MailgunClient with proper parameter forwarding.


1-19: Well-documented adapter implementation.

The eslint exception for filename is appropriately documented, and the class structure with constants and JSDoc provides clear context for the adapter's purpose and behavior.

ghost/core/core/server/adapters/email-analytics/EmailAnalyticsBase.js (2)

16-22: Solid base class constructor.

The use of Object.defineProperty to create a non-writable requiredFns array enforces the adapter contract effectively, and the default config handling is clean.


24-52: Excellent documentation and contract enforcement.

The comprehensive JSDoc with usage examples clearly defines the adapter contract, and the default implementation properly enforces that subclasses must implement fetchLatest().

ghost/core/test/unit/server/adapters/email-analytics/mailgun/index.test.js (1)

1-248: Comprehensive test coverage for Mailgun adapter.

The test suite thoroughly validates constructor behavior, option handling, timestamp conversion, event filtering, tag management, and adapter contract compliance. Well-structured with appropriate mocking.

ghost/core/core/server/services/adapter-manager/index.js (3)

19-19: Clean adapter registration.

The email-analytics adapter registration follows the established pattern and properly references the base class for contract enforcement.


22-28: Well-documented API extension.

The updated documentation clearly explains the new runtimeConfig parameter and its purpose for dependency injection, with updated examples including the new email-analytics adapter.


33-37: Runtime config merging works correctly for the current use case.

The shallow merge using Object.assign is appropriate for dependency injection where runtime config provides top-level objects (config, settings). The precedence order correctly allows runtime config to override file-based config.

Note: If future adapters require deep merging of nested configuration objects, this implementation would need to be enhanced. For the current dependency injection use case, shallow merging is sufficient.

@danielraffel
Copy link
Author

Fixed all issues:

Critical fix:

  1. Optional chaining - Added options?.begin, options?.end, and options?.maxEvents to prevent TypeError when options is undefined

Code quality improvements:
2. require placement - Moved require('@tryghost/errors') to top of mailgun adapter
3. Test coverage - Added test case for undefined options parameter

All changes pushed in commit a02259f.

ref TryGhost#21684

Implements Mailgun as an email provider adapter to work with Ghost's
AdapterManager system. This enables future extensibility for other
email providers (SendGrid, SES, Postmark) while maintaining full
backward compatibility with existing Mailgun functionality.

Key changes:
- Created Mailgun adapter at adapters/email/mailgun/index.js extending EmailProviderBase
- Removed legacy MailgunEmailProvider from services/email-service/ (single source of truth)
- Enhanced AdapterManager to support runtime config injection for dependency injection
- Added resetCacheFor() method to AdapterManager for proper encapsulation
- Added safeguard to resetCacheFor() that throws for unknown adapter types (matches getAdapter pattern)
- Fixed caching to clear cache when runtime config provided (prevents stale dependencies)
- Updated EmailServiceWrapper to use adapter pattern
- Added email adapter default configuration (defaults to Mailgun)
- Comprehensive test coverage: 23 adapter tests + 2 resetCacheFor tests (all passing)

Technical notes:
- Adapter file must be index.js for Node.js module resolution (email/mailgun → email/mailgun/index.js)
- Runtime config uses resetCacheFor() to maintain encapsulation vs direct instanceCache access
- resetCacheFor() throws NotFoundError for unknown types to catch bugs early (consistent with getAdapter)
- Maintains all existing Mailgun features: batch sending, recipient variables, delivery scheduling, error handling
- Use replaceAll() for global token replacement
- Add guards for undefined replacements and replacement definitions
- Default options parameter and destructure recipients/replacementDefinitions
- Redact PII from error details (limit to 2000 chars)
- Add defensive check for response.id
- Wrap error handler in try-catch for safety
- Remove redundant logging.info() call
- Add mailgunClient interface validation in constructor
ref TryGhost#21684

Implements email analytics as an adapter to work with Ghost's AdapterManager
system. This enables future extensibility for other email analytics providers
while maintaining full backward compatibility with existing Mailgun analytics.

Key changes:
- Created EmailAnalyticsBase at adapters/email-analytics/EmailAnalyticsBase.js
- Registered 'email-analytics' adapter type with AdapterManager
- Implemented Mailgun analytics adapter at adapters/email-analytics/mailgun/index.js
- Removed legacy EmailAnalyticsProviderMailgun from services/email-analytics/
- Updated EmailAnalyticsServiceWrapper to use adapter pattern
- Enhanced AdapterManager to support runtime config injection
- Added email-analytics adapter default configuration (defaults to Mailgun)
- Comprehensive test coverage with 24 passing tests (7 base + 17 Mailgun)

Technical notes:
- Adapter file must be index.js for Node.js module resolution
- Runtime config (config, settings) injected via AdapterManager
- Maintains all existing Mailgun analytics features: event fetching, pagination, custom tags
- fetchLatest() interface supports batch processing of email events (delivered, opened, failed, etc.)
- Moved require('@tryghost/errors') to top of mailgun adapter
- Added optional chaining for options.begin, options.end, options.maxEvents
- Added test case for undefined options parameter
@danielraffel danielraffel force-pushed the adapter/email-analytics branch from a02259f to 02da44f Compare October 25, 2025 22:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community [triage] Community features and bugs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant