Skip to content

Instantly share code, notes, and snippets.

@jahands
Last active October 30, 2024 21:13
Show Gist options
  • Save jahands/d03646e0365d3b578d0aa528f0f3590e to your computer and use it in GitHub Desktop.
Save jahands/d03646e0365d3b578d0aa528f0f3590e to your computer and use it in GitHub Desktop.
Workflows auto capture to Sentry inside step.do() callback
import { NonRetryableError } from 'cloudflare:workflows'
import { initWorkflowSentry } from '../helpers/sentry'
import type { WorkflowEvent, WorkflowStep, WorkflowStepConfig } from 'cloudflare:workers'
import type { Toucan } from 'toucan-js'
import type { Bindings } from '../types'
/** Workflow context (similar to Hono context) */
export class WorkflowContext<Params = unknown> {
readonly ctx: ExecutionContext
readonly env: Bindings
readonly event: WorkflowEvent<Params>
readonly step: WorkflowStep
readonly sentry: Toucan
// TODO: Add logger
constructor(c: {
ctx: ExecutionContext
env: Bindings
event: WorkflowEvent<Params>
step: WorkflowStep
}) {
this.ctx = c.ctx
this.env = c.env
this.event = c.event
this.step = c.step
this.sentry = initWorkflowSentry(c.env, c.ctx)
}
/**
* Run a Workflows Step.
*
* Callers should avoid capturing `StepFailedError` and `NonRetryableError`
* because these errors are already captured to Sentry within this method.
*/
do<T extends Rpc.Serializable<T>>(name: string, callback: () => Promise<T>): Promise<T>
do<T extends Rpc.Serializable<T>>(
name: string,
config: WorkflowStepConfig,
callback: () => Promise<T>
): Promise<T>
async do<T extends Rpc.Serializable<T>>(
name: string,
configOrCallback: WorkflowStepConfig | (() => Promise<T>),
callback?: () => Promise<T>
): Promise<T> {
let config: WorkflowStepConfig
let cb: () => Promise<T>
if (typeof configOrCallback === 'function') {
cb = configOrCallback
config = {} // default to empty
} else {
if (!callback) {
throw new Error('missing callback')
}
config = configOrCallback
cb = callback
}
return await this.step.do(name, config, async () => {
try {
return await cb()
} catch (e) {
this.sentry.captureException(e)
if (e instanceof NonRetryableError) {
throw e
} else {
throw new StepFailedError(e instanceof Error ? `${e.name}: ${e.message}` : 'unknown')
}
}
})
}
}
/**
* StepFailedError that should be thrown within
* do() when we don't want to capture to Sentry
*/
export class StepFailedError extends Error {}
import type { MyWorkflowParams } from './workflows/my-workflow/my-workflow'
export type Bindings = {
MyWorkflow: Workflow<MyWorkflowParams>
KV: KVNamespace
}
// type-safe version of Workflow binding type (wanted typed params)
interface Workflow<Params = unknown> {
/**
* Get a handle to an existing instance of the Workflow.
* @param id Id for the instance of this Workflow
* @returns A promise that resolves with a handle for the Instance
*/
get(id: string): Promise<WorkflowInstance>
/**
* Create a new instance and return a handle to it. If a provided id exists, an error will be thrown.
* @param options Options when creating an instance including id and params
* @returns A promise that resolves with a handle for the Instance
*/
create(options?: WorkflowInstanceCreateOptions<Params>): Promise<WorkflowInstance>
}
interface WorkflowInstanceCreateOptions<Params> {
/**
* An id for your Workflow instance. Must be unique within the Workflow.
*/
id?: string
/**
* The event payload the Workflow instance is triggered with
*/
params?: Params
}
import { WorkflowEntrypoint } from 'cloudflare:workers'
import { NonRetryableError } from 'cloudflare:workflows'
import { z } from 'zod'
import { StepFailedError, WorkflowContext } from '../context'
import type { WorkflowEvent, WorkflowStep } from 'cloudflare:workers'
import type { Bindings } from '../../types'
export type MyWorkflowParams = z.infer<typeof MyWorkflowParams>
export const MyWorkflowParams = z.object({
someParam: z.string(),
})
export class MyWorkflow extends WorkflowEntrypoint<Bindings, MyWorkflowParams> {
async run(event: WorkflowEvent<MyWorkflowParams>, step: WorkflowStep) {
const c = new WorkflowContext({
ctx: this.ctx,
env: this.env,
event,
step,
})
try {
const params = MyWorkflowParams.parse(event.payload)
await c.do('some action that may throw error', async () => {
console.log(params.someParam)
// Do something that may fail
})
} catch (e) {
// Step errors are already recorded within step.do()
if (!(e instanceof StepFailedError) && !(e instanceof NonRetryableError)) {
c.sentry.captureException(e)
}
throw e
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment