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

2.1.5 2026-04-22 13:24 UTC

This package is auto-updated.

Last update: 2026-04-22 19:25:36 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. Names APM transactions via newrelic_name_wt().
EventApiClientHTTP transport via New Relic Event API. Works without the PHP extension. nameTransaction is a no-op.
NewRelicClientFactoryFactory that selects PHP agent or Event API client based on config.
TelemetryManagerOrchestrator — builds payloads, dispatches events and APM attributes.
CheckoutContextBuilderSingle authoritative source for the telemetry contract fields.
TrafficClassifierClassifies requests as human / bot / internal / qa / developer / agentic AI.
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, APM transaction name
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. 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 sends CronJobExecution events via the New Relic Event API — complementary to the OrderAbandoned events this module emits during cron.

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
CartAbandonedCron: draft orders that never entered checkoutHuman 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:

ContextTransaction name
Checkout route requestcheckout_telemetry/checkout/{step} (e.g., checkout_telemetry/checkout/payment)
Cron: abandoned order detectioncheckout_telemetry/cron/order_abandoned
Cron: abandoned cart detectioncheckout_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:

ClassificationMeaning
humanReal customer traffic
friendly_botKnown search engine / social bots
hostile_botSuspicious automated clients
agentic_aiAI-powered shopping agents (detected by commerce_agent_detector or UA)
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 > 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:

  1. commerce_agent_detector integration — If the drupal-mods/commerce-agent-detector module is installed, its detection result is read from the request attribute automatically.
  2. Custom headers — Requests with X-Agent-ID or X-ACP-Version headers are classified as agentic AI.
  3. 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:

  1. Set as a New Relic Browser agent custom attribute (if the NR browser snippet is present).
  2. Sent to the backend via a nrct_cid cookie.
  3. 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

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

  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

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:

  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.
  9. APM step latency — Timeseries of p95 response time faceted by checkout_telemetry/checkout/* transaction names.
  10. 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

  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 — ✅ Implemented. Cron detects carts that never entered checkout, filtered to human traffic only.

  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 — ✅ Implemented. HTTP transport via the New Relic Event API. Works without the PHP extension.

  6. Real User Monitoring correlation — ✅ Implemented. A JavaScript library generates a shared browserCorrelationId for frontend-backend NRQL joins.

  7. Alerting templates — ✅ Implemented. See Alerting templates section above.

  8. Agentic AI classification — ✅ Implemented. AI shopping agents are classified as agentic_ai via commerce_agent_detector integration, custom headers, and configurable UA patterns.

  9. Checkout flow visualization — Build a NR dashboard widget that visualizes step-by-step progression rates per checkout flow ID.

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