Is it possible to intercept Workflow exceptions and send them to Sentry?

First I tried implementing WorkflowInboundCallsInterceptor interface

import * as Sentry from "@sentry/node"
import { Next, WorkflowExecuteInput, WorkflowInboundCallsInterceptor } from "@temporalio/workflow"

export default class SentryInboundWorkflowInterceptor implements WorkflowInboundCallsInterceptor {
  constructor(public readonly workflowType: string) {
    Sentry.init({
      dsn: "https://something@something.ingest.sentry.io/123456789",
      tracesSampleRate: 1.0,
    })
  }

  public async execute(
    input: WorkflowExecuteInput,
    next: Next<WorkflowInboundCallsInterceptor, "execute">
  ): Promise<unknown> {
    try {
      return await next(input)
    } catch (error) {
      Sentry.captureException(error)
      throw error
    }
  }
}

and this as the interceptors module:

import { WorkflowInterceptorsFactory, workflowInfo } from "@temporalio/workflow"
import SentryInboundWorkflowInterceptor from "./workflow_inbound_sentry_implementation"

export const interceptors: WorkflowInterceptorsFactory = () => ({
  inbound: [new SentryInboundWorkflowInterceptor(workflowInfo().workflowType)],
})

and finally send create the worker like this:

  // ...
  const workerOptions: WorkerOptions = {
    connection,
    activities,
    workflowsPath: require.resolve("./workflows"),
    taskQueue: settings.craSharedQueue,
    interceptors: {
      workflowModules: [require.resolve("../../interceptors/workflow_interceptors")],
    }
  }
  const worker = await Worker.create(workerOptions)

  await worker.run()
  // ...

But I’m getting this warning, and of course I can’t see exceptions on Sentry:

 Connecting to temporal server at: host.docker.internal:7233
 Error: Your Workflow code (or a library used by your Workflow code) is importing the following disallowed modules:
   - 'os'
   - 'util'
   - 'fs'
   - 'path'
   - 'url'
   - 'http'
   - 'https'
   - 'stream'
   - 'zlib'
   - 'child_process'
   - 'inspector'
   - 'domain'
   - 'async_hooks'
   - 'net'
   - 'tls'
   - 'events'
 These modules can't be used in workflow context as they might break determinism.HINT: Consider the following options:
  • Make sure that activity code is not imported from workflow code. Use `import type` to import activity function signatures.
  • Move code that has non-deterministic behaviour to activities.
  • If you know for sure that a disallowed module will not be used at runtime, add its name to 'WorkerOptions.bundlerOptions.ignoreModules' in order to dismiss this warning.
 See also: https://typescript.temporal.io/api/interfaces/worker.workeroptions/#bundleroptions and https://docs.temporal.io/typescript/determinism.
     at /home/node/app/node_modules/@temporalio/worker/src/workflow/bundler.ts:277:27
     at finalCallback (/home/node/app/node_modules/webpack/lib/Compiler.js:441:32)
     at /home/node/app/node_modules/webpack/lib/Compiler.js:505:17
     at /home/node/app/node_modules/webpack/lib/HookWebpackError.js:68:3
     at Hook.eval [as callAsync] (eval at create (/home/node/app/node_modules/tapable/lib/HookCodeFactory.js:33:10), <anonymous>:6:1)
     at Hook.CALL_ASYNC_DELEGATE [as _callAsync] (/home/node/app/node_modules/tapable/lib/Hook.js:18:14)
     at Cache.storeBuildDependencies (/home/node/app/node_modules/webpack/lib/Cache.js:122:37)
     at /home/node/app/node_modules/webpack/lib/Compiler.js:501:19
     at Hook.eval [as callAsync] (eval at create (/home/node/app/node_modules/tapable/lib/HookCodeFactory.js:33:10), <anonymous>:6:1)
     at Hook.CALL_ASYNC_DELEGATE [as _callAsync] (/home/node/app/node_modules/tapable/lib/Hook.js:18:14)

I’ve read both Interface: WorkflowInboundCallsInterceptor | Temporal TypeScript SDK API Reference and this other post: Catching errors from a workflow like activity interceptors do but still no luck…

1 Like

Workflow Interceptors are executed inside the Workflow sandbox, and therefore need to play with the same constraints as Workflow code regarding not importing non-deterministic code (see related doc page).

@sentry/node is obviously a non-deterministic module, as it is performing network access, reading configuration files, starting child process, etc.

As a general rule, non-deterministic code should be moved to activities or local activities. However, the TS SDK also provide a different mechanism, sinks, that is better suited for logging, observability and similar use cases.

Sinks are similar to Activities in that they are both registered on the Worker and proxied into the Workflow. However, they differ from Activities in important ways:

  • Sink functions don’t return any value back to the Workflow and cannot not be awaited.
  • Sink calls are not recorded in Workflow histories (no timeouts or retries).
  • Sink functions are always run on the same Worker that runs the Workflow they are called from.

See this related subsection in our documentation.

Okay, so correct me if I’m wrong, from what I understood from it, we should rely on a big try/catch block around the workflow function and within the catch block call the function we get from proxySinks<MyOwnSinksDefinition>(), something along the lines of:

const { sentry } = proxySinks<MySinks>()

export async function workflowFunction(): Promise<void> { 
  try {
    // ... working code
    throw ApplicationFailure.nonRetryable("MySpecialNonRetryableError")
    // ... working code
  } catch (error) { 
    sentry.captureException(error) // Call the sink method that relays to `Sentry.captureException`
    throw error
  }
}

I was expecting a “catch all” approach that we kinda can have with ActivityInboundCallsInterceptor for activities

we should rely on a big try/catch block around the workflow function and within the catch block call the function we get from proxySinks<MyOwnSinksDefinition>() ,

I was expecting a “catch all” approach that we kinda can have with ActivityInboundCallsInterceptor for activities

You can do the same with a WorkflowInboundCallsInterceptor. What you had in your initial post was correct from the “error catching” perspective. It’s only the use of the Sentry library that was problematic, which you can replace by the call to a sentry sink function, like you did in your last post.

1 Like