drupal-mods/newrelic_checkout_telemetry

Instruments Drupal Commerce checkout flow with New Relic custom events and APM transaction attributes for production observability.

Maintainers

Package info

gitlab.com/drupal-mods/new-relic-checkout-telemetry

Issues

Type:drupal-module

pkg:composer/drupal-mods/newrelic_checkout_telemetry

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 1

1.1.0 2026-04-03 11:30 UTC

This package is auto-updated.

Last update: 2026-04-03 17:30:13 UTC


README

Pipeline License

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

DependencySupported versions
Drupal core10.x, 11.x
Drupal Commerce2.x+
PHP8.1+
New Relic PHP agentAny (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

  1. Place this module in your modules/custom/ directory.
  2. Enable via Drush:

    drush en newrelic_checkout_telemetry
    
  3. Configure at Commerce > Configuration > New Relic Checkout Telemetry (/admin/commerce/config/newrelic-checkout-telemetry).

Architecture

Service layer

ServicePurpose
NewRelicClientThin wrapper around NR PHP functions. Fails silently if extension absent.
TelemetryManagerOrchestrator — builds payloads, dispatches events and APM attributes.
CheckoutContextBuilderSingle authoritative source for the telemetry contract fields.
TrafficClassifierClassifies requests as human / bot / internal / qa / developer.
InternalTrafficResolverResolves internal tester status via roles, UIDs, email domains, IPs.
ErrorNormalizerNormalizes exception messages for stable dashboard grouping.

Event subscribers

SubscriberListens toEmits
KernelRequestSubscriberkernel.request, kernel.response on checkout routesCheckoutStarted, CheckoutStepViewed, CheckoutStepCompleted, APM attributes
KernelExceptionSubscriberkernel.exception on commerce routesCheckoutStepFailed, PaymentAuthFailed
CommerceCheckoutSubscribercommerce_order.place.post_transitionCheckoutStepCompleted (final step)
CommerceOrderSubscriberOrder state transitions (place, fulfill, cancel, validate)OrderPlaced, OrderCompleted, OrderAbandoned
CommercePaymentSubscriberPayment state transitions (authorize, capture, void)PaymentAuthStarted, PaymentAuthSucceeded, PaymentCaptured, PaymentVoided
CommerceCartSubscriberCartEvents::CART_ORDER_ITEM_ADDAPM 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 nameTriggerNotes
CheckoutStartedFirst GET to a checkout route per order per sessionSession-flag deduplication
CheckoutStepViewedGET request to commerce_checkout.formOne per step per page load
CheckoutStepCompletedPOST+redirect on checkout form; order place transitionRedirect heuristic + place transition
CheckoutStepFailedKernel exception on checkout routesSafety net for unhandled errors
PaymentAuthStartedPayment authorize pre-transitionOnly fires for gateways using state machine
PaymentAuthFailedKernel exception on payment routesCaught by exception subscriber
PaymentAuthSucceededPayment authorize post-transition to authorization/completed
OrderPlacedOrder place post-transitionMost important business event
OrderCompletedOrder fulfill post-transition
OrderAbandonedOrder cancel transition; cron idle detectionTwo 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:

ClassificationMeaning
humanReal customer traffic
friendly_botKnown search engine / social bots
hostile_botSuspicious automated clients
internal_testInternal tester (by role, email, uid, IP, or header)
qa_testQA team member (by role)
developer_testDeveloper (by role or non-production environment)
unknownUnable 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

  1. CheckoutStepViewed/CheckoutStepCompleted per-step: Commerce checkout does not fire granular Symfony events per step. We use route matching on commerce_checkout.form and POST+redirect heuristics. This is reliable for standard checkout flows but may miss AJAX-based step transitions.

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

  3. PaymentAuthStarted: Only fires when payment gateways use the state machine authorize transition. Off-site gateways that create payments directly in an authorized state will skip this event.

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

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

  6. Off-site payment returns: Return callbacks from off-site gateways may not carry the same session/user context as the original checkout.

Deployment

  1. Deploy the module code.
  2. Enable: drush en newrelic_checkout_telemetry
  3. Configure at /admin/commerce/config/newrelic-checkout-telemetry.
  4. Verify events appear in New Relic within a few minutes.

Rollback

  1. Disable: drush pmu newrelic_checkout_telemetry
  2. Remove module code.
  3. The module has no schema changes — disabling is safe and immediate.
  4. State entries (newrelic_checkout_telemetry.abandoned_flagged) can be cleaned via drush 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:

  1. Checkout health overview — Billboards for: checkout starts (human), orders placed, conversion rate, payment failure rate.
  2. Checkout funnel — Funnel visualization: Started → Reviewed → Placed.
  3. Error trends — Timeseries of CheckoutStepFailed + PaymentAuthFailed, faceted by human vs bot.
  4. Traffic classification — Pie chart of clientClassification across all events.
  5. Payment provider health — Table of auth success rate by provider.
  6. Top errors — Table of top error groups with first/last seen.
  7. Internal vs external — Stacked timeseries comparing internal test traffic to real customer traffic.
  8. 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

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

  2. Cart abandonment — Extend cron detection to track carts that never entered checkout (requires tracking cart creation time).

  3. Revenue telemetry — ✅ Implemented. Order total bucket, currency, and item count are included in telemetry payloads.

  4. Custom Drupal event — ✅ Implemented. A TelemetryPreRecordEvent is dispatched before each custom event is recorded.

  5. Event API transport — Add an alternate transport that sends events via the New Relic Event API (HTTP) for environments without the PHP agent.

  6. Real User Monitoring correlation — Add a JavaScript component that passes the NR browser session ID to the backend for frontend-backend correlation.

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