DEV Community

Cover image for I Added Sentry to a WooCommerce Membership Site. It Caught a Real Bug Within the Hour.
Vicente G. Reyes
Vicente G. Reyes

Posted on • Originally published at vicentereyes.org

I Added Sentry to a WooCommerce Membership Site. It Caught a Real Bug Within the Hour.

I'm the lead developer on a WooCommerce/Divi/LearnDash membership platform. Like most mature WordPress sites, it runs a lot of plugins — page builder, LMS, membership gating, marketing automation, forms, SMTP — and when something breaks, the failure usually surfaces as a vague member complaint days later, not as an actionable error with a stack trace.

I finally fixed that by wiring up Sentry with the WordPress Sentry plugin (wp-sentry-integration). Setup took about twenty minutes. The first real bug showed up in the feed within the hour — an error loop that had been silently firing on every hit to a REST endpoint, and that nobody would ever have reported, because it never broke anything a user could see.

This post covers the setup, the gotchas, and the first catch.

Why the wp-sentry plugin and not the raw SDK snippets

When you create a Sentry project, the onboarding page hands you a \Sentry\init() snippet for PHP and a loader <script> tag for the browser. You don't want either of them on a WordPress site.

The wp-sentry-integration plugin bundles both official SDKs and does the integration work you'd otherwise hand-roll:

  • Initializes the PHP SDK early enough in the WordPress bootstrap to catch fatals from other plugins
  • Hooks PHP errors, exceptions, and fatal errors
  • Enqueues the browser SDK on the frontend and wires up tracing and Session Replay
  • Attaches WordPress context to every event: the logged-in user, environment, release, and plugin/theme metadata

The part that confused me at first: the plugin's settings page has checkboxes, and none of them are clickable. That's by design. The plugin has zero UI configuration. Everything is driven by constants in wp-config.php, and the settings page is a read-only dashboard that reports which constants it detected. Checkbox ticked means the constant exists and is valid. You can click them all day and nothing happens.

I've come around to this being the right call. Your error-tracking config lives in code, survives plugin updates, can't be broken from wp-admin, and a compromised admin account can't quietly redirect your error stream to someone else's DSN. Same philosophy as DISABLE_WP_CRON.

Setup

1. Two Sentry projects, not one

Create separate projects for PHP and browser JavaScript. On a plugin-heavy WooCommerce site, browser noise — extension errors, ancient browsers, third-party scripts — will drown out real server-side errors if they share a project. Splitting them keeps triage sane and lets you set different alert rules and quotas per side.

2. Constants in wp-config.php

Everything goes above the /* That's all, stop editing! */ line:

// --- Sentry ---
define( 'WP_SENTRY_PHP_DSN', 'https://xxxx@oXXXX.ingest.us.sentry.io/PHP_PROJECT_ID' );
define( 'WP_SENTRY_BROWSER_DSN', 'https://xxxx@oXXXX.ingest.us.sentry.io/JS_PROJECT_ID' );

define( 'WP_SENTRY_ENV', 'production' );

// Attach the logged-in WP user (id/username/email) to events
define( 'WP_SENTRY_SEND_DEFAULT_PII', true );

// Keep tracing cheap — it eats quota fast
define( 'WP_SENTRY_TRACES_SAMPLE_RATE', 0.05 );
define( 'WP_SENTRY_BROWSER_TRACES_SAMPLE_RATE', 0.05 );

// Session Replay: no ambient recording, full replay on error
define( 'WP_SENTRY_BROWSER_REPLAYS_SESSION_SAMPLE_RATE', 0 );
define( 'WP_SENTRY_BROWSER_REPLAYS_ON_ERROR_SAMPLE_RATE', 1.0 );
Enter fullscreen mode Exit fullscreen mode

One trap: the browser value must be the DSN from Settings → Client Keys, not the CDN loader script URL that Sentry's JS onboarding page shows you. They look superficially similar. The DSN has the https://key@org.ingest...sentry.io/project_id shape.

A few decisions worth explaining:

  • SEND_DEFAULT_PII on a membership site is genuinely useful — you can tie a checkout error directly to the member who reported it instead of playing email tag. The tradeoff is that member emails now live in Sentry, so lock down team access and account for it in your privacy policy.
  • Replay sample rates of 0 / 1.0 means no routine session recording, but when a JS error fires you get the full replay of what the user did leading up to it. On checkout flows this is gold. Sentry masks all text and inputs by default; verify that before trusting it near payment forms.
  • Skip profiling. It requires the excimer PHP extension, which most managed WordPress hosts don't ship. Tracing plus replay covers what you actually need.

3. Verify

Reload the plugin's tools page — the checkboxes should now be ticked and the test buttons active. Fire the PHP test event and the browser test error, and confirm each lands in the correct project. The browser test on my setup also captured a session replay, which confirmed the whole replay pipeline in one shot.

4. Turn on inbound filters immediately

In the JS project: Settings → Inbound Filters → enable the browser-extension, legacy-browser, and web-crawler filters. Do this on day one, not after your quota is gone.

The first catch

Within the hour, a real issue appeared in the PHP project:

Exception: Scheduled action for bwfcrm_broadcast_run_queue will not be
executed as no callbacks are registered.
Enter fullscreen mode Exit fullscreen mode

Firing from GET /wp-json/woofunnels/v1/worker — FunnelKit's background worker endpoint.

Reading the trace

The exception came from Action Scheduler's safety check:

if ( ! has_action( $hook ) ) {
    throw new Exception( 'Scheduled action ... will not be executed
        as no callbacks are registered.' );
}
Enter fullscreen mode Exit fullscreen mode

Action Scheduler refuses to run a scheduled job that has no listeners — it treats a dangling hook as a broken state and throws.

The hook, bwfcrm_broadcast_run_queue, belongs to FunnelKit's CRM/broadcast module. The site doesn't use email broadcasts (I confirmed with the client — a one-line message, thirty seconds), so nothing registers that hook anymore. But the recurring action for it still lived in FunnelKit's custom action store, scheduled to run every 60 seconds.

Sentry's SQL breadcrumbs made the loop visible: the worker fetches the action, the execution throws, and the fetch path re-schedules the next occurrence. An orphaned job, erroring and rebooking itself forever. It had presumably been doing this since whenever the CRM module was deactivated — invisible, because the exception is handled and nothing user-facing ever broke.

This is exactly the class of bug error monitoring exists for. No member would ever report it. No admin screen showed it. It was just background waste: a query cycle and a thrown exception on every worker run, on a site that already had a history of Action Scheduler bloat.

The fix: delete the work, keep a guard

Two parts.

Delete the orphaned recurring action. The table and hook name come straight from the event's own breadcrumbs, so the query is exact — but look before you delete:

wp db query "SELECT id, hook, status, recurring_interval, group_slug
  FROM wp_bwf_actions
  WHERE hook = 'bwfcrm_broadcast_run_queue';"

wp db query "DELETE FROM wp_bwf_actions
  WHERE hook = 'bwfcrm_broadcast_run_queue';"
Enter fullscreen mode Exit fullscreen mode

(Adjust the table prefix for your install.) This isn't as scary as deleting from an unknown table sounds: the plugin's own queue runner deletes rows from this table constantly as part of normal processing — the last breadcrumb in the Sentry event was literally a DELETE on the same row. Worst case, re-enabling the CRM module re-registers the hook and re-seeds the schedule from scratch. There's no unrecoverable state in a row that says "run this hook every 60 seconds."

Register a no-op guard. Some plugins have a habit of re-seeding their recurring actions on init or after updates. If that happens here, I don't want the error loop back — so I registered a listener that does nothing:

// Guard: orphaned FunnelKit CRM broadcast hook.
// The bwfcrm_broadcast_run_queue recurring action has no registered
// callback (CRM/broadcasts unused — confirmed with the client).
// This no-op prevents Action Scheduler from throwing if the action
// gets re-seeded. Safe to remove if FunnelKit CRM is ever activated.
add_action( 'bwfcrm_broadcast_run_queue', '__return_null' );
Enter fullscreen mode Exit fullscreen mode

__return_null is a WordPress core utility whose entire body is return null;. Registering it means has_action() returns true, the safety check passes, and a re-seeded action "runs" harmlessly instead of restarting the error stream.

I put this in Code Snippets rather than the theme's functions.php — it's an infrastructure fix, not presentation code, and it should survive theme switches, stay individually toggleable, and be findable by name by the next developer (or by me in six months). The comment block matters as much as the code: a bare add_action( 'something', '__return_null' ) with no explanation is a trap for whoever finds it later.

Then: resolve the issue in Sentry. If the action ever re-seeds and the guard is somehow gone, Sentry reopens the issue and I know immediately — that's regression detection for free.

Takeaways

  • On WordPress, configure Sentry through the wp-sentry plugin's constants, not the onboarding snippets. The plugin is the engine; wp-config.php is the ignition key.
  • Split PHP and browser into separate projects and enable inbound filters on the JS side before the noise starts.
  • Handled exceptions are still signal. The bug I found threw a caught exception — nothing crashed, no user saw anything. Without monitoring it would have run forever.
  • Sentry breadcrumbs can hand you the fix. The SQL breadcrumbs contained the exact table, the exact row, and proof that deleting the row was an operation the plugin itself performs routinely.
  • Fix orphaned scheduled actions in two layers: delete the orphan (stop the waste), and register a no-op guard (stay fixed if the plugin re-seeds it). Then resolve the issue in Sentry and let it be your regression alarm.

Total time from "install the plugin" to "real production bug found, diagnosed from the stack trace and breadcrumbs, fixed with a two-layer patch": about an hour. That's the fastest an observability tool has ever paid for itself on one of my projects.

Top comments (0)