Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Temporal Sentry Laravel Package

vanta/temporal-sentry

Laravel integration that connects Temporal workflows and activities to Sentry, automatically capturing exceptions with useful workflow context, retry details, and worker information so production failures are easier to monitor, trace, and debug, reducing time spent correlating logs across queues and...

View on GitHub
Deep Wiki
Context7

Getting Started

composer require vanta/temporal-sentry
php artisan vendor:publish --tag=temporal-sentry-config

Prerequisites — get both halves working independently first, or you'll be debugging two systems at once:

  1. sentry/sentry-laravel installed and reporting a plain HTTP 500 from a controller.
  2. Your Temporal client/worker booting and executing a trivial workflow (whatever package you use for the SDK — temporal/sdk, a Laravel wrapper, or a RoadRunner worker binary).

Then wire the package in. In a standard Laravel install the service provider is auto-discovered; the config file is where the real decisions live.

Where to look first, in order:

  • config/temporal-sentry.php — the toggles that matter are the ones separating workflow capture from activity capture, the sampling rate, and the list of task queues to instrument. Most "it doesn't report anything" issues are a queue not in that list.
  • The service provider — see what it binds and, critically, whether it decorates your Temporal client factory or expects you to opt in manually by wrapping interceptors. If it's the latter, nothing happens until you register it.
  • The interceptor/middleware classes — these are where the Sentry hub is opened per task and where trace headers are injected/extracted. Read them once; you'll be subclassing or ordering them later.
  • The README's example worker bootstrap — copy it verbatim before customizing.

First use case to validate end-to-end: an activity that throws a plain RuntimeException. You want exactly one Sentry event, tagged with the workflow id, run id, activity type, attempt number, and task queue. If you get that, tracing and scoping are almost certainly wired correctly. If you get three events, jump to Gotchas.

Implementation Patterns

Register the interceptor on both client and worker. Outbound headers are attached by the client interceptor; inbound extraction happens on the worker. Registering only the worker gives you events with no trace continuity to the request that started the workflow. Registering only the client gives you a trace that dead-ends.

Per-task scope, not per-worker scope. Temporal workers are long-lived processes handling many tasks sequentially. The pattern that works:

  • Open a Sentry scope/hub at task start, seed it with workflow id, run id, activity type, task queue, attempt.
  • Do the work.
  • Flush and reset in a finally — never let tags from task A bleed into task B.

Anything that pushes context into a static/global hub must be popped. Treat the hub as request-scoped even though you're not in a request.

Correlate via headers + search attributes. Put sentry-trace and baggage in the Temporal headers so child workflows and activities inherit the trace, and additionally copy the trace id into a Temporal search attribute. That second step is what lets you go from "Sentry alert" to "which workflow run was this" without grepping logs. Headers alone are invisible in the Temporal UI.

Activity wrapper for controlled capture. For activities where you want richer context than the interceptor provides:

try {
    return $this->doWork($input);
} catch (\Throwable $e) {
    \Sentry\withScope(function ($scope) use ($e, $ctx) {
        $scope->setContext('temporal', [
            'workflow_id' => $ctx->workflowId,
            'run_id' => $ctx->runId,
            'activity' => $ctx->activityType,
            'attempt' => $ctx->attempt,
        ]);
        \Sentry\captureException($e);
    });
    throw $e; // preserve Temporal's retry semantics — never swallow
}

The rethrow is non-negotiable. Capturing without rethrowing converts a retryable activity into a silent success.

Retry-aware deduplication. Activities retry by design; a flaky downstream gives you N events for one logical failure. Two workable strategies:

  • Capture only on the last attempt (compare attempt to the retry policy's maximumAttempts).
  • Capture every attempt but tag attempt, and use Sentry's fingerprinting (\Sentry\configureScope + setFingerprint) keyed on workflow_type + activity_type + exception class so retries collapse into one issue.

Pick one per task queue. Mixing them produces confusing issue counts.

Separate expected failures from infrastructure failures. ApplicationFailure (non-retryable business validation) shouldn't page anyone. Filter by exception type and nonRetryable before capture — either in your wrapper or in before_send. This is the single highest-leverage tuning knob; without it the integration becomes noise within a week and the team mutes it.

Schedules and cron workflows. If you use Temporal Schedules, tag events with the schedule id and the scheduled action. A failing nightly reconciliation is otherwise indistinguishable from a failing on-demand run of the same workflow type.

Local/test environments. Disable via env in phpunit.xml rather than relying on DSN absence, and assert in at least one test that the interceptor is registered — this is the thing that silently regresses when someone refactors the worker bootstrap.

Gotchas and Tips

Scope leakage across tasks is the number-one bug. Long-running workers (RoadRunner, Swoole, Octane-style runtimes, or plain while loops) don't reset Sentry's global state between tasks. Symptom: events attributed to the wrong workflow, or a stack of stale breadcrumbs from a task that finished ten minutes ago. Always finally { reset scope; }\. If the package opens a hub but doesn't close it, wrap it.

Double reporting. sentry/sentry-laravel's exception handler already reports uncaught exceptions in the process. If the interceptor also captures, you get two events per failure with different fingerprints. Decide which layer owns activity failures and disable the other — usually you want the interceptor for context and the global handler left on for genuinely unexpected worker-level crashes.

Trace breaks at the usual boundaries. ChildWorkflow, signals, continue-as-new, and query handlers are the common places headers get dropped. If spans suddenly stop nesting, instrument those transitions first. Also verify the interceptor ordering — a header-injecting interceptor that runs before Temporal serializes the payload is useless.

continue-as-new resets your correlation. The workflow id survives; the run id does not. Don't key dashboards on run id alone.

Payload exposure is a real risk. Temporal inputs are often full DTOs containing tokens, PII, or customer data. If the package attaches activity arguments to the Sentry context, you are shipping that to a third party. Audit what gets serialized, then add a before_send / before_breadcrumb scrubber. Prefer logging ids and types over payloads.

Don't use workflow_id as a Sentry tag casually. Tag values are indexed and high-cardinality tags degrade Sentry's issue grouping and make tag filters unusable. Put ids in setContext (searchable in the event detail, not indexed as tags) and reserve tags for low-cardinality dimensions: task queue, workflow type, activity type, environment.

Flush before the process sleeps. In worker runtimes where the process persists, buffered events may not ship until shutdown, and shutdown may be SIGKILL. Flush at the end of each task if your event volume is low; flush at a batch boundary if it isn't.

Debugging checklist when nothing appears:

  1. DSN actually set in the worker's environment — workers frequently run in a different container/process than the web app and don't inherit .env.
  2. Task queue present in the config's instrumented list.
  3. Sampling rate — 0.0 in staging is a classic.
  4. Interceptor registered on the worker bootstrap, not just the client.
  5. Sentry\captureException called inside the SDK's flush window; add a Sentry\flush() after a deliberate throw in a smoke-test activity.

Treat this dependency as fragile. One star, a single release, zero dependents means it has not been battle-tested across SDK versions, and Temporal's interceptor API plus Sentry's SDK both move. Pin the exact version, and isolate it: keep interceptor registration in your own service provider or a thin SentryTemporal facade class so replacing it with 40 lines of custom interceptor is a one-file change rather than a refactor. Verify compatibility against your specific Temporal SDK version before committing — the last release date on the package tells you nothing about which Temporal release it targets.

Extension points worth knowing: interceptor ordering (subclass the package's interceptor rather than replacing it, so you keep header propagation while adding tags), before_send for filtering and scrubbing, and per-task-queue sampling if you run noisy high-throughput queues alongside critical low-throughput ones. The last one is worth setting up on day one — it's much harder to add once alert fatigue has set in.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin