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 || ^3
- 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
Suggests
- drupal-mods/newrelic_cron: Instruments Drupal cron execution and alerts on failures, slow runs, and missing runs via the New Relic Event API.
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. Names APM transactions via newrelic_name_wt(). |
EventApiClient | HTTP transport via New Relic Event API. Works without the PHP extension. nameTransaction is a no-op. |
NewRelicClientFactory | Factory that selects PHP agent or Event API client based on config. |
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 / agentic AI. |
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, APM transaction name |
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.
It also detects abandoned carts (draft orders that never entered checkout,
filtered to human traffic only) and emits CartAbandoned events.
Both cron jobs name their APM transaction (checkout_telemetry/cron/order_abandoned
and checkout_telemetry/cron/cart_abandoned) so they appear with distinct,
meaningful names in New Relic APM instead of the generic Drupal cron transaction.
Tip: Pair with drupal-mods/newrelic_cron to monitor cron execution health (failures, slow runs, missing runs). That module wraps
Drupal::cron()->run()with timing and sendsCronJobExecutionevents via the New Relic Event API — complementary to theOrderAbandonedevents this module emits during cron.
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 |
CartAbandoned | Cron: draft orders that never entered checkout | Human traffic only; configurable threshold |
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,
isAgenticAi, agentName, browserCorrelationId,
orderTotalBucket, orderCurrency, orderItemCount,
timestamp, moduleVersion
Error events additionally include: errorClass, errorMessageNormalized,
exceptionFingerprint, httpStatusCode.
APM transaction naming
The module calls newrelic_name_wt() (via NewRelicClientInterface::nameTransaction()) to
give APM transactions meaningful names:
| Context | Transaction name |
|---|---|
| Checkout route request | checkout_telemetry/checkout/{step} (e.g., checkout_telemetry/checkout/payment) |
| Cron: abandoned order detection | checkout_telemetry/cron/order_abandoned |
| Cron: abandoned cart detection | checkout_telemetry/cron/cart_abandoned |
This is a no-op when using the Event API transport (no PHP agent available) and fails silently in all cases.
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 |
agentic_ai | AI-powered shopping agents (detected by commerce_agent_detector or UA) |
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 > agentic AI > hostile bot UA > friendly bot UA > internal IP > human.
Agentic AI detection
Agentic AI traffic (AI shopping agents, copilots, etc.) is classified automatically via three methods:
- commerce_agent_detector integration — If the
drupal-mods/commerce-agent-detectormodule is installed, its detection result is read from the request attribute automatically. - Custom headers — Requests with
X-Agent-IDorX-ACP-Versionheaders are classified as agentic AI. - Configurable UA patterns — User-agent substrings (e.g., ChatGPT, ClaudeBot, GPTBot) are matched case-insensitively.
When agentic AI is detected, events include isAgenticAi = 1 and
agentName (when available). The customerType is set to agentic_ai.
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
- AI agent patterns: UA substrings for agentic AI detection (ChatGPT, ClaudeBot, etc.)
Configuration
Feature toggles
Each event type can be individually enabled/disabled. The master enabled
switch disables all telemetry when off.
Abandonment detection
- Order abandonment threshold: Hours of idle time before a draft order in checkout is considered abandoned (default: 24h).
- Order abandonment batch size: Max orders processed per cron run (default: 100).
- Cart abandonment threshold: Hours of idle time before a draft order that never entered checkout is considered abandoned (default: 48h). Only human traffic is considered.
- Cart abandonment batch size: Max carts processed per cron run (default: 100).
Transport
Two transport modes are available:
- PHP Agent (default) — Uses the New Relic PHP extension (
newrelic_record_custom_event). Requires the extension to be installed. Events are silently dropped if absent. - Event API — Sends events via HTTP POST to the New Relic Event API. Works without the PHP extension. Requires a New Relic Ingest (License) API key and Account ID. Supports US and EU regions.
Switch transports in the admin form under Transport.
Browser (RUM) correlation
When enabled, a lightweight JavaScript library is attached to checkout
pages. It generates a session-scoped browserCorrelationId UUID that is:
- Set as a New Relic Browser agent custom attribute (if the NR browser snippet is present).
- Sent to the backend via a
nrct_cidcookie. - Included in all backend telemetry events as
browserCorrelationId.
This enables NRQL joins between frontend PageView/PageAction events and backend custom events:
SELECT *
FROM PageView JOIN CheckoutStepViewed
ON PageView.checkoutCorrelationId = CheckoutStepViewed.browserCorrelationId
WHERE checkoutCorrelationId IS NOT NULL
SINCE 1 day ago
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. Cart abandonment (orders that never entered checkout) is also detected separately, filtered to human traffic only. 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
APM transaction throughput by checkout step
SELECT count(*)
FROM Transaction
WHERE name LIKE 'checkout_telemetry/checkout/%'
FACET name
SINCE 1 day ago
TIMESERIES AUTO
APM response time by checkout step
SELECT average(duration) * 1000 AS 'avg ms'
FROM Transaction
WHERE name LIKE 'checkout_telemetry/checkout/%'
FACET name
SINCE 1 day ago
TIMESERIES AUTO
Cron job execution time (abandoned detection)
SELECT average(duration) * 1000 AS 'avg ms', max(duration) * 1000 AS 'max ms', count(*) AS 'runs'
FROM Transaction
WHERE name IN (
'checkout_telemetry/cron/order_abandoned',
'checkout_telemetry/cron/cart_abandoned'
)
FACET name
SINCE 7 days ago
Slow checkout steps (p95 latency)
SELECT percentile(duration, 95) * 1000 AS 'p95 ms'
FROM Transaction
WHERE name LIKE 'checkout_telemetry/checkout/%'
FACET name
SINCE 1 day ago
Cart abandonment volume (human traffic)
SELECT count(*)
FROM CartAbandoned
WHERE customerType = 'human'
FACET storeName
SINCE 7 days ago
TIMESERIES AUTO
Agentic AI checkout activity
SELECT count(*)
FROM CheckoutStarted, OrderPlaced
WHERE isAgenticAi = 1
FACET agentName
SINCE 7 days ago
TIMESERIES AUTO
Agentic AI vs human conversion rate
SELECT
percentage(
filter(count(*), WHERE checkoutOutcome = 'placed'),
filter(count(*), WHERE checkoutOutcome = 'started')
) AS 'Conversion %'
FROM CheckoutStarted, OrderPlaced
FACET cases(
WHERE isAgenticAi = 1 AS 'Agentic AI',
WHERE clientClassification = 'human' AS 'Human'
)
SINCE 7 days ago
Browser-to-backend correlation
SELECT count(*)
FROM CheckoutStepViewed
WHERE browserCorrelationId IS NOT NULL
FACET checkoutStep
SINCE 1 day 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.
- APM step latency — Timeseries of p95 response time faceted by
checkout_telemetry/checkout/*transaction names. - Cron job health — Table of avg/max duration for
checkout_telemetry/cron/*transactions.
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 payment attempts over a 5-minute sliding window.
SELECT
percentage(count(*), WHERE errorClass IS NOT NULL)
FROM PaymentAuthStarted, PaymentAuthFailed, PaymentAuthSucceeded
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
(SELECT uniqueCount(orderId) FROM OrderPlaced WHERE clientClassification = 'human' SINCE 30 minutes ago)
/ (SELECT uniqueCount(orderId) FROM CheckoutStarted WHERE clientClassification = 'human' SINCE 30 minutes ago)
* 100 AS 'conversionRate'
- 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 checkoutOutcome = 'payment_authorized')
FROM PaymentAuthStarted, PaymentAuthSucceeded, PaymentAuthFailed
FACET paymentProvider
WHERE clientClassification = 'human'
SINCE 15 minutes ago
- Threshold: Static, below 90 for at least 5 minutes
- Window duration: 15 minutes (sliding)
- Facet:
paymentProvider(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— ✅ Implemented. Cron detects carts that never entered checkout, filtered to human traffic only.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— ✅ Implemented. HTTP transport via the New Relic Event API. Works without the PHP extension.Real User Monitoring correlation— ✅ Implemented. A JavaScript library generates a sharedbrowserCorrelationIdfor frontend-backend NRQL joins.Alerting templates— ✅ Implemented. See Alerting templates section above.Agentic AI classification— ✅ Implemented. AI shopping agents are classified asagentic_aivia commerce_agent_detector integration, custom headers, and configurable UA patterns.Checkout flow visualization — Build a NR dashboard widget that visualizes step-by-step progression rates per checkout flow ID.
A/B test correlation — Integrate with Drupal A/B testing modules to include experiment variant IDs in telemetry payloads.
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.