drupal-mods / newrelic_checkout_telemetry
Instruments Drupal Commerce checkout flow with New Relic custom events and APM transaction attributes for production observability.
Package info
gitlab.com/drupal-mods/new-relic-checkout-telemetry
Type:drupal-module
pkg:composer/drupal-mods/newrelic_checkout_telemetry
Requires
- php: ^8.1
- drupal/commerce: ^2
- drupal/core: ^10 || ^11
Requires (Dev)
- drupal/coder: ^8.3
- mglaman/phpstan-drupal: ^1.2
- phpstan/extension-installer: ^1.3
- phpstan/phpstan: ^1.10
- phpstan/phpstan-deprecation-rules: ^1.1
- phpunit/phpunit: ^10.5 || ^11
README
A Drupal Commerce module that instruments the checkout flow with New Relic custom events and APM transaction attributes. Designed for production use on ecommerce sites that need to distinguish real customer-impacting checkout issues from bot traffic, internal testing, and expected noise.
Compatibility
| Dependency | Supported versions |
|---|---|
| Drupal core | 10.x, 11.x |
| Drupal Commerce | 2.x+ |
| PHP | 8.1+ |
| New Relic PHP agent | Any (module degrades gracefully if absent) |
Requirements
- Drupal 10.x or 11.x
- Drupal Commerce 2.x+ (commerce_checkout, commerce_order, commerce_payment, commerce_cart)
- state_machine module (ships with Commerce)
- New Relic PHP agent (module degrades gracefully if absent)
Installation
Via Composer (recommended)
composer require drupal-mods/newrelic_checkout_telemetry
drush en newrelic_checkout_telemetry
Manual
- Place this module in your
modules/custom/directory. Enable via Drush:
drush en newrelic_checkout_telemetryConfigure at Commerce > Configuration > New Relic Checkout Telemetry (
/admin/commerce/config/newrelic-checkout-telemetry).
Architecture
Service layer
| Service | Purpose |
|---|---|
NewRelicClient | Thin wrapper around NR PHP functions. Fails silently if extension absent. |
TelemetryManager | Orchestrator — builds payloads, dispatches events and APM attributes. |
CheckoutContextBuilder | Single authoritative source for the telemetry contract fields. |
TrafficClassifier | Classifies requests as human / bot / internal / qa / developer. |
InternalTrafficResolver | Resolves internal tester status via roles, UIDs, email domains, IPs. |
ErrorNormalizer | Normalizes exception messages for stable dashboard grouping. |
Event subscribers
| Subscriber | Listens to | Emits |
|---|---|---|
KernelRequestSubscriber | kernel.request, kernel.response on checkout routes | CheckoutStarted, CheckoutStepViewed, CheckoutStepCompleted, APM attributes |
KernelExceptionSubscriber | kernel.exception on commerce routes | CheckoutStepFailed, PaymentAuthFailed |
CommerceCheckoutSubscriber | commerce_order.place.post_transition | CheckoutStepCompleted (final step) |
CommerceOrderSubscriber | Order state transitions (place, fulfill, cancel, validate) | OrderPlaced, OrderCompleted, OrderAbandoned |
CommercePaymentSubscriber | Payment state transitions (authorize, capture, void) | PaymentAuthStarted, PaymentAuthSucceeded, PaymentCaptured, PaymentVoided |
CommerceCartSubscriber | CartEvents::CART_ORDER_ITEM_ADD | APM attributes on cart transactions |
Cron
hook_cron() detects abandoned orders (draft orders idle past a configurable
threshold that entered checkout) and emits OrderAbandoned events.
Custom events emitted
| Event name | Trigger | Notes |
|---|---|---|
CheckoutStarted | First GET to a checkout route per order per session | Session-flag deduplication |
CheckoutStepViewed | GET request to commerce_checkout.form | One per step per page load |
CheckoutStepCompleted | POST+redirect on checkout form; order place transition | Redirect heuristic + place transition |
CheckoutStepFailed | Kernel exception on checkout routes | Safety net for unhandled errors |
PaymentAuthStarted | Payment authorize pre-transition | Only fires for gateways using state machine |
PaymentAuthFailed | Kernel exception on payment routes | Caught by exception subscriber |
PaymentAuthSucceeded | Payment authorize post-transition to authorization/completed | |
OrderPlaced | Order place post-transition | Most important business event |
OrderCompleted | Order fulfill post-transition | |
OrderAbandoned | Order cancel transition; cron idle detection | Two detection methods |
Telemetry contract
Every event includes these normalized fields (when available):
cartId, orderId, checkoutStep, checkoutOutcome, paymentProvider,
customerType, isInternalTester, environment, routeName, requestPath,
requestMethod, storeId, storeName, orderType, orderState, checkoutFlowId,
hasAccount, isAuthenticated, userIdHash, clientClassification,
isBotSuspected, isQaTraffic, isDeveloperTraffic, isInternalIp,
orderTotalBucket, orderCurrency, orderItemCount,
timestamp, moduleVersion
Error events additionally include: errorClass, errorMessageNormalized,
exceptionFingerprint, httpStatusCode.
Revenue fields:
orderTotalBucket— Range bucket (e.g.$25-50,$100-250,$1000+). Exact amounts are never sent.orderCurrency— ISO 4217 currency code (e.g.USD).orderItemCount— Number of line items in the order.
PII safety
- User IDs are one-way hashed with Drupal's
hash_salt. - No emails, payment tokens, cardholder data, or addresses are sent.
- Exception messages are normalized to strip IDs, amounts, and timestamps.
- Stack traces are never sent as event payloads.
Extending telemetry payloads
A TelemetryPreRecordEvent is dispatched before every custom event is
recorded. Other modules can subscribe to enrich or modify the payload:
// In mymodule.services.yml:
// mymodule.telemetry_subscriber:
// class: Drupal\mymodule\EventSubscriber\TelemetrySubscriber
// tags:
// - { name: event_subscriber }
namespace Drupal\mymodule\EventSubscriber;
use Drupal\newrelic_checkout_telemetry\Event\TelemetryPreRecordEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class TelemetrySubscriber implements EventSubscriberInterface {
public static function getSubscribedEvents(): array {
return [TelemetryPreRecordEvent::class => 'onPreRecord'];
}
public function onPreRecord(TelemetryPreRecordEvent $event): void {
$event->setAttribute('abTestVariant', 'checkout-v2');
$event->setAttribute('shippingMethod', 'express');
}
}
Traffic classification
Requests are classified into one of:
| Classification | Meaning |
|---|---|
human | Real customer traffic |
friendly_bot | Known search engine / social bots |
hostile_bot | Suspicious automated clients |
internal_test | Internal tester (by role, email, uid, IP, or header) |
qa_test | QA team member (by role) |
developer_test | Developer (by role or non-production environment) |
unknown | Unable to classify |
Classification priority: internal test header > internal tester > non-prod environment > hostile bot UA > friendly bot UA > internal IP > human.
Configuration
All classification rules are configurable in the admin form:
- Bot user-agents: Friendly bot UA substrings (Googlebot, Bingbot, etc.)
- Hostile bot patterns: Suspicious UA substrings (python-requests, curl/, etc.)
- Internal test header: Custom HTTP header (e.g.,
X-Internal-Test) - Internal roles: Drupal role machine names
- Internal user IDs: Specific UIDs
- Internal email domains: Email domain patterns (e.g.,
mycompany.com) - Internal IP ranges: CIDR notation (e.g.,
10.0.0.0/8) - Non-production environments: Environment names treated as dev traffic
Configuration
Feature toggles
Each event type can be individually enabled/disabled. The master enabled
switch disables all telemetry when off.
Abandonment detection
- Threshold: Hours of idle time before a draft order in checkout is considered abandoned (default: 24h).
- Batch size: Max orders processed per cron run (default: 100).
Logging
Three levels: none, errors (default), all (verbose, for debugging).
Limitations and edge cases
CheckoutStepViewed/CheckoutStepCompleted per-step: Commerce checkout does not fire granular Symfony events per step. We use route matching on
commerce_checkout.formand POST+redirect heuristics. This is reliable for standard checkout flows but may miss AJAX-based step transitions.CheckoutStarted: No single "started" event exists in Commerce. We use session-flag deduplication on first checkout route visit per order. If sessions are not available (e.g., headless/API), this event won't fire.
PaymentAuthStarted: Only fires when payment gateways use the state machine
authorizetransition. Off-site gateways that create payments directly in an authorized state will skip this event.PaymentAuthFailed: Payment declines caught by Commerce's own error handling (DeclineException, PaymentGatewayException) may not propagate to the kernel exception handler. The KernelExceptionSubscriber is a safety net, not a complete solution. Consider adding gateway-specific event subscribers for complete payment failure coverage.
OrderAbandoned (cron): Detects idle draft orders in checkout. Does not detect users who abandon before reaching checkout (cart abandonment). State-based deduplication uses Drupal State API with bounded list.
Off-site payment returns: Return callbacks from off-site gateways may not carry the same session/user context as the original checkout.
Deployment
- Deploy the module code.
- Enable:
drush en newrelic_checkout_telemetry - Configure at
/admin/commerce/config/newrelic-checkout-telemetry. - Verify events appear in New Relic within a few minutes.
Rollback
- Disable:
drush pmu newrelic_checkout_telemetry - Remove module code.
- The module has no schema changes — disabling is safe and immediate.
- State entries (
newrelic_checkout_telemetry.abandoned_flagged) can be cleaned viadrush state:delete newrelic_checkout_telemetry.abandoned_flagged.
Example NRQL queries
Checkout starts by traffic classification
SELECT count(*)
FROM CheckoutStarted
FACET clientClassification
SINCE 1 day ago
TIMESERIES AUTO
Checkout failures by step
SELECT count(*)
FROM CheckoutStepFailed
FACET checkoutStep
SINCE 7 days ago
TIMESERIES AUTO
Payment failures by provider
SELECT count(*)
FROM PaymentAuthFailed
FACET paymentProvider, errorClass
SINCE 7 days ago
Order completion rate (placed vs started)
SELECT
filter(count(*), WHERE checkoutOutcome = 'started') AS 'Started',
filter(count(*), WHERE checkoutOutcome = 'placed') AS 'Placed',
percentage(
filter(count(*), WHERE checkoutOutcome = 'placed'),
filter(count(*), WHERE checkoutOutcome = 'started')
) AS 'Conversion %'
FROM CheckoutStarted, OrderPlaced
WHERE clientClassification = 'human'
SINCE 7 days ago
Human vs bot checkout error trends
SELECT count(*)
FROM CheckoutStepFailed
FACET cases(
WHERE isBotSuspected = 1 AS 'Bot',
WHERE clientClassification = 'human' AS 'Human',
WHERE clientClassification LIKE '%test%' AS 'Internal'
)
SINCE 7 days ago
TIMESERIES AUTO
Internal test traffic vs external traffic
SELECT count(*)
FROM CheckoutStarted, OrderPlaced, CheckoutStepFailed
FACET cases(
WHERE isInternalTester = 1 AS 'Internal',
WHERE isInternalTester = 0 AS 'External'
)
SINCE 24 hours ago
TIMESERIES 1 hour
Top normalized error groups
SELECT count(*), latest(timestamp)
FROM CheckoutStepFailed, PaymentAuthFailed
FACET errorClass, errorMessageNormalized
SINCE 7 days ago
LIMIT 25
First seen / last seen analysis
SELECT min(timestamp) AS 'First Seen', max(timestamp) AS 'Last Seen', count(*)
FROM CheckoutStepFailed
FACET exceptionFingerprint, errorMessageNormalized
SINCE 30 days ago
LIMIT 50
Checkout funnel (human traffic only)
SELECT
filter(count(*), WHERE checkoutStep = 'login' OR checkoutStep = 'order_information') AS 'Info Step',
filter(count(*), WHERE checkoutStep = 'review') AS 'Review Step',
filter(count(*), WHERE checkoutStep = 'complete') AS 'Complete'
FROM CheckoutStepCompleted
WHERE clientClassification = 'human'
SINCE 7 days ago
Payment success rate by provider
SELECT
filter(count(*), WHERE paymentProvider IS NOT NULL) AS 'Auth Attempts',
percentage(
filter(count(*), WHERE checkoutOutcome = 'payment_authorized'),
filter(count(*), WHERE paymentProvider IS NOT NULL)
) AS 'Success Rate'
FROM PaymentAuthStarted, PaymentAuthSucceeded
FACET paymentProvider
SINCE 7 days ago
Orders placed from internal IPs
SELECT count(*)
FROM OrderPlaced
WHERE isInternalIp = 1
FACET environment, customerType
SINCE 30 days ago
Abandoned orders by checkout step
SELECT count(*)
FROM OrderAbandoned
FACET checkoutStep
SINCE 7 days ago
Dashboard recommendations
Build a New Relic dashboard with these widgets:
- Checkout health overview — Billboards for: checkout starts (human), orders placed, conversion rate, payment failure rate.
- Checkout funnel — Funnel visualization: Started → Reviewed → Placed.
- Error trends — Timeseries of CheckoutStepFailed + PaymentAuthFailed, faceted by human vs bot.
- Traffic classification — Pie chart of clientClassification across all events.
- Payment provider health — Table of auth success rate by provider.
- Top errors — Table of top error groups with first/last seen.
- Internal vs external — Stacked timeseries comparing internal test traffic to real customer traffic.
- Abandonment — Timeseries of OrderAbandoned faceted by checkout step.
Alerting templates
Below are ready-to-use NRQL alert conditions for common checkout monitoring scenarios. Create these in New Relic Alerts & AI → Policies → Add a condition → NRQL.
Payment failure spike
Triggers when the payment failure rate exceeds 10% of all checkout attempts over a 5-minute sliding window.
SELECT
percentage(count(*), WHERE eventName = 'PaymentAuthFailed')
FROM CheckoutStepCompleted, PaymentAuthFailed
WHERE clientClassification = 'human'
SINCE 5 minutes ago
- Threshold: Static, above 10 for at least 3 minutes
- Window duration: 5 minutes (sliding)
Conversion rate drop
Alerts when the checkout-to-order conversion rate drops below its baseline. Compare orders placed vs checkouts started.
SELECT
filter(uniqueCount(orderId), WHERE eventName = 'OrderPlaced')
/ filter(uniqueCount(orderId), WHERE eventName = 'CheckoutStarted')
* 100 AS 'conversionRate'
FROM OrderPlaced, CheckoutStarted
WHERE clientClassification = 'human'
SINCE 30 minutes ago
- Threshold: Baseline (lower), deviation of 2 standard deviations
- Window duration: 30 minutes (sliding)
Error rate threshold
Fires when checkout step errors exceed a fixed count per minute.
SELECT count(*)
FROM CheckoutStepFailed
WHERE clientClassification = 'human'
SINCE 5 minutes ago
- Threshold: Static, above 25 for at least 3 minutes
- Window duration: 5 minutes (sliding)
Abandonment spike
Detects when cart abandonment volume rises sharply.
SELECT count(*)
FROM OrderAbandoned
SINCE 1 hour ago
COMPARE WITH 1 day ago
- Threshold: Baseline (upper), deviation of 3 standard deviations
- Window duration: 1 hour (sliding)
- Evaluation offset: 15 minutes (allows cron time to detect)
Payment gateway degradation
Monitors per-gateway auth success rate.
SELECT
percentage(count(*), WHERE eventName = 'OrderPlaced')
FROM OrderPlaced, PaymentAuthFailed
FACET paymentGateway
SINCE 15 minutes ago
- Threshold: Static, below 90 for at least 5 minutes
- Window duration: 15 minutes (sliding)
- Facet:
paymentGateway(creates per-gateway signals)
Suggested next enhancements
Gateway-specific payment failure subscribers — Add listeners for specific payment gateway events (e.g., Stripe webhooks) to capture decline reasons that never reach Commerce's state machine.
Cart abandonment — Extend cron detection to track carts that never entered checkout (requires tracking cart creation time).
Revenue telemetry— ✅ Implemented. Order total bucket, currency, and item count are included in telemetry payloads.Custom Drupal event— ✅ Implemented. ATelemetryPreRecordEventis dispatched before each custom event is recorded.Event API transport — Add an alternate transport that sends events via the New Relic Event API (HTTP) for environments without the PHP agent.
Real User Monitoring correlation — Add a JavaScript component that passes the NR browser session ID to the backend for frontend-backend correlation.
Alerting templates— ✅ Implemented. See Alerting templates section above.
Development
Setup
git clone git@gitlab.com:drupal-mods/new-relic-checkout-telemetry.git
cd new-relic-checkout-telemetry
composer install
Quality checks
# Run everything (PHPCS + PHPStan + PHPUnit).
composer ci
# Individual tools:
composer phpcs # Drupal coding standards
composer phpcbf # Auto-fix CS violations
composer phpstan # Static analysis (level 6)
composer test # PHPUnit unit tests
Testing guidance
Unit tests cover core services in isolation (ErrorNormalizer, NewRelicClient, TrafficClassifier, TelemetryManager). These run without a Drupal bootstrap.
The following areas require kernel or integration tests within a full Drupal site:
- Config schema validation (
KernelTestBase). - Event subscriber wiring and dispatch (
KernelTestBase). - Settings form rendering and submission (
BrowserTestBase). - Cron hook execution with entity storage (
KernelTestBase).
License
This project is licensed under the GPL-2.0-or-later license. See LICENSE for details.