|
| 1 | +import * as Sentry from '@sentry/cloudflare' |
| 2 | + |
| 3 | +import type { DurableObject, WorkflowEntrypoint } from 'cloudflare:workers' |
| 4 | +import type { SharedHonoEnv } from '../types' |
| 5 | + |
| 6 | +type SentryInstrumentationOptions = { |
| 7 | + /** |
| 8 | + * Custom sentry options |
| 9 | + * |
| 10 | + * Sentry will only be instrumented if SENTRY_DSN is provided in the environment. |
| 11 | + * |
| 12 | + * @default Handlers/DOs: |
| 13 | + * ```ts |
| 14 | + * { |
| 15 | + * tracesSampleRate: 0.02, |
| 16 | + * sendDefaultPii: true, |
| 17 | + * } |
| 18 | + * ``` |
| 19 | + * |
| 20 | + * @default Workflows: |
| 21 | + * ```ts |
| 22 | + * { |
| 23 | + * tracesSampleRate: 1.0, |
| 24 | + * sendDefaultPii: true, |
| 25 | + * } |
| 26 | + * ``` |
| 27 | + */ |
| 28 | + sentry?: Sentry.CloudflareOptions |
| 29 | +} |
| 30 | + |
| 31 | +export type InstrumentHandlerOptions< |
| 32 | + Env extends SharedHonoEnv = SharedHonoEnv, |
| 33 | + QueueHandlerMessage = unknown, |
| 34 | + CfHostMetadata = unknown, |
| 35 | +> = SentryInstrumentationOptions & { |
| 36 | + /** |
| 37 | + * All exported handlers (supported handlers will be instrumented) |
| 38 | + */ |
| 39 | + handler: ExportedHandler<Env, QueueHandlerMessage, CfHostMetadata> |
| 40 | +} |
| 41 | + |
| 42 | +/** |
| 43 | + * Instruments a Cloudflare Workers handler with Sentry tracing. |
| 44 | + * |
| 45 | + * This wraps your default export handler to automatically capture errors and trace requests. |
| 46 | + * Sentry instrumentation is only applied if SENTRY_DSN is provided in the environment. |
| 47 | + * |
| 48 | + * @param options - Configuration options including the handler and optional Sentry settings |
| 49 | + * @returns The instrumented handler ready to be exported |
| 50 | + * |
| 51 | + * @example |
| 52 | + * ```typescript |
| 53 | + * import { instrumentHandler } from '@repo/hono-helpers' |
| 54 | + * |
| 55 | + * const handler: ExportedHandler<Env> = { |
| 56 | + * async fetch(request, env, ctx) { |
| 57 | + * return new Response('Hello') |
| 58 | + * } |
| 59 | + * } |
| 60 | + * |
| 61 | + * export default instrumentHandler({ |
| 62 | + * handler, |
| 63 | + * sentry: { tracesSampleRate: 0.02 } |
| 64 | + * }) |
| 65 | + * ``` |
| 66 | + * |
| 67 | + * @disclaimer docstring by Claude Sonnet 4.5 via AmpCode |
| 68 | + */ |
| 69 | +export function instrumentHandler< |
| 70 | + Env extends SharedHonoEnv, |
| 71 | + QueueHandlerMessage = unknown, |
| 72 | + CfHostMetadata = unknown, |
| 73 | +>( |
| 74 | + options: InstrumentHandlerOptions<Env, QueueHandlerMessage, CfHostMetadata> |
| 75 | +): ExportedHandler<Env, QueueHandlerMessage, CfHostMetadata> { |
| 76 | + const instrumented = Sentry.withSentry<Env, QueueHandlerMessage, CfHostMetadata>( |
| 77 | + (env: Env) => |
| 78 | + ({ |
| 79 | + dsn: env.SENTRY_DSN, |
| 80 | + environment: env.ENVIRONMENT, |
| 81 | + release: env.SENTRY_RELEASE, |
| 82 | + tracesSampleRate: 0.02, |
| 83 | + sendDefaultPii: true, |
| 84 | + ...options.sentry, |
| 85 | + }) satisfies Sentry.CloudflareOptions, |
| 86 | + options.handler |
| 87 | + ) |
| 88 | + |
| 89 | + return instrumented |
| 90 | +} |
| 91 | + |
| 92 | +/** |
| 93 | + * Type alias for a Durable Object class constructor signature. |
| 94 | + * |
| 95 | + * We need this because we're typing the **constructor** (the class itself), not the instance. |
| 96 | + * When you write `instrumentDO(CounterDO, ...)`, you're passing the class/constructor, |
| 97 | + * not an instance. This type describes what that **constructor** must look like: |
| 98 | + * - Takes `(ctx: DurableObjectState, env: Env)` as parameters |
| 99 | + * - Returns an instance of `DurableObject<Env>` |
| 100 | + * |
| 101 | + * We explicitly define this to: |
| 102 | + * 1. Match Sentry's `instrumentDurableObjectWithSentry` type expectations |
| 103 | + * 2. Allow TypeScript to properly infer the environment type `Env` from the constructor |
| 104 | + * 3. Make the `instrumentDO` signature readable instead of repeating this constructor type |
| 105 | + * |
| 106 | + * @disclaimer docstring by Claude Sonnet 4.5 via AmpCode |
| 107 | + */ |
| 108 | +type DOClass< |
| 109 | + Env extends SharedHonoEnv, |
| 110 | + Props = unknown, |
| 111 | +> = new (ctx: DurableObjectState<Props>, env: Env) => DurableObject<Env, Props> |
| 112 | + |
| 113 | +export type InstrumentDOOptions = SentryInstrumentationOptions |
| 114 | + |
| 115 | +/** |
| 116 | + * Instruments a Durable Object class with Sentry tracing. |
| 117 | + * |
| 118 | + * This wraps your DO class to automatically capture errors and trace requests to the DO. |
| 119 | + * Sentry instrumentation is only applied if SENTRY_DSN is provided in the environment. |
| 120 | + * |
| 121 | + * **Important:** Your DO class must extend `DurableObject<Env>` from `cloudflare:workers`, |
| 122 | + * not just implement the old `DurableObject` interface. |
| 123 | + * |
| 124 | + * @param doClass - The Durable Object class to instrument (the class itself, not an instance) |
| 125 | + * @param options - Optional configuration for Sentry instrumentation |
| 126 | + * @returns The instrumented DO class with the same type signature |
| 127 | + * |
| 128 | + * @example |
| 129 | + * ```typescript |
| 130 | + * import { DurableObject } from 'cloudflare:workers' |
| 131 | + * import { instrumentDO } from '@repo/hono-helpers' |
| 132 | + * |
| 133 | + * class MyDO extends DurableObject<Env> { |
| 134 | + * override async fetch(request: Request) { |
| 135 | + * return new Response('Hello from DO') |
| 136 | + * } |
| 137 | + * } |
| 138 | + * |
| 139 | + * export const MyDurableObject = instrumentDO<Env>(MyDO, { |
| 140 | + * sentry: { tracesSampleRate: 0.02 } |
| 141 | + * }) |
| 142 | + * ``` |
| 143 | + * |
| 144 | + * @disclaimer docstring by Claude Sonnet 4.5 via AmpCode |
| 145 | + */ |
| 146 | +export function instrumentDO< |
| 147 | + Env extends SharedHonoEnv, |
| 148 | + C extends DOClass<Env> = DOClass<Env>, |
| 149 | +>(doClass: C, options?: InstrumentDOOptions): C { |
| 150 | + const instrumented = Sentry.instrumentDurableObjectWithSentry( |
| 151 | + (env: Env) => |
| 152 | + ({ |
| 153 | + dsn: env.SENTRY_DSN, |
| 154 | + environment: env.ENVIRONMENT, |
| 155 | + release: env.SENTRY_RELEASE, |
| 156 | + tracesSampleRate: 0.02, |
| 157 | + sendDefaultPii: true, |
| 158 | + ...options?.sentry, |
| 159 | + }) satisfies Sentry.CloudflareOptions, |
| 160 | + doClass |
| 161 | + ) |
| 162 | + |
| 163 | + return instrumented |
| 164 | +} |
| 165 | + |
| 166 | +/** |
| 167 | + * Type alias for a Workflow class constructor signature. |
| 168 | + * |
| 169 | + * Similar to DOClass, this types the **constructor** (the class itself), not the instance. |
| 170 | + * When you write `instrumentWorkflow(MyWorkflow, ...)`, you're passing the class/constructor. |
| 171 | + * |
| 172 | + * @disclaimer docstring by Claude Sonnet 4.5 via AugmentCode |
| 173 | + */ |
| 174 | +type WorkflowClass<Env extends SharedHonoEnv, Params = unknown> = new ( |
| 175 | + ctx: ExecutionContext, |
| 176 | + env: Env |
| 177 | +) => WorkflowEntrypoint<Env, Params> |
| 178 | + |
| 179 | +export type InstrumentWorkflowOptions = SentryInstrumentationOptions |
| 180 | + |
| 181 | +/** |
| 182 | + * Instruments a Cloudflare Workflow class with Sentry. |
| 183 | + * |
| 184 | + * This wraps your Workflow class to automatically capture errors and trace workflow execution. |
| 185 | + * The workflow's instanceId is used to generate a deterministic trace_id to link all steps together. |
| 186 | + * Sentry instrumentation is only applied if SENTRY_DSN is provided in the environment. |
| 187 | + * |
| 188 | + * **Important:** Your Workflow class must extend `WorkflowEntrypoint<Env, Params>` from `cloudflare:workers`. |
| 189 | + * |
| 190 | + * **Note:** Create spans only inside `step.do()` callbacks. Due to workflow hibernation, code outside |
| 191 | + * `step.do()` may be re-executed, leading to duplicated spans. |
| 192 | + * |
| 193 | + * @param workflowClass - The Workflow class to instrument (the class itself, not an instance) |
| 194 | + * @param options - Optional configuration for Sentry instrumentation |
| 195 | + * @returns The instrumented Workflow class with the same type signature |
| 196 | + * |
| 197 | + * @example |
| 198 | + * ```typescript |
| 199 | + * import { WorkflowEntrypoint } from 'cloudflare:workers' |
| 200 | + * import { instrumentWorkflow } from '@repo/hono-helpers' |
| 201 | + * |
| 202 | + * class MyWorkflowBase extends WorkflowEntrypoint<Env, Params> { |
| 203 | + * async run(event, step) { |
| 204 | + * await step.do('fetch data', async () => { |
| 205 | + * // your code here |
| 206 | + * }) |
| 207 | + * } |
| 208 | + * } |
| 209 | + * |
| 210 | + * export const MyWorkflow = instrumentWorkflow<Env, Params>(MyWorkflowBase, { |
| 211 | + * sentry: { tracesSampleRate: 1.0 } |
| 212 | + * }) |
| 213 | + * ``` |
| 214 | + * |
| 215 | + * @disclaimer docstring by Claude Sonnet 4.5 via AugmentCode |
| 216 | + */ |
| 217 | +export function instrumentWorkflow< |
| 218 | + Env extends SharedHonoEnv, |
| 219 | + Params = unknown, |
| 220 | + C extends WorkflowClass<Env, Params> = WorkflowClass<Env, Params>, |
| 221 | +>(workflowClass: C, options?: InstrumentWorkflowOptions): C { |
| 222 | + const instrumented = Sentry.instrumentWorkflowWithSentry( |
| 223 | + (env: Env) => |
| 224 | + ({ |
| 225 | + dsn: env.SENTRY_DSN, |
| 226 | + environment: env.ENVIRONMENT, |
| 227 | + release: env.SENTRY_RELEASE, |
| 228 | + tracesSampleRate: 1.0, |
| 229 | + sendDefaultPii: true, |
| 230 | + ...options?.sentry, |
| 231 | + }) satisfies Sentry.CloudflareOptions, |
| 232 | + workflowClass |
| 233 | + ) |
| 234 | + |
| 235 | + return instrumented |
| 236 | +} |
0 commit comments