Skip to content

Commit d87fd03

Browse files
committed
WIP claude code tried
1 parent e091787 commit d87fd03

File tree

14 files changed

+515
-6
lines changed

14 files changed

+515
-6
lines changed

CLAUDE.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,41 @@
4545
- Changesets manage versions and changelogs
4646
</architecture>
4747

48+
<sentry-integration>
49+
**Optional Sentry error tracking and monitoring** - enabled by setting SENTRY_DSN
50+
51+
Three instrumentation patterns available in `@repo/hono-helpers`:
52+
1. **`instrumentHandler()`** - Wraps ExportedHandler with Sentry (for handlers, queues, emails, etc.)
53+
- Default: 2% trace sampling rate
54+
- Usage: `export default instrumentHandler({ handler, sentry: { tracesSampleRate: 0.02 } })`
55+
56+
2. **`instrumentDO()`** - Wraps Durable Object classes
57+
- Default: 2% trace sampling rate
58+
- Usage: `export const MyDO = instrumentDO<Env>(MyDOClass, { sentry: { tracesSampleRate: 0.02 } })`
59+
60+
3. **`instrumentWorkflow()`** - Wraps Workflow classes
61+
- Default: 100% trace sampling rate (workflows are important!)
62+
- Usage: `export const MyWorkflow = instrumentWorkflow<Env, Params>(MyWorkflowClass)`
63+
64+
**Middleware:**
65+
- `withSentry({ op: 'http.server' })` - Adds request-level tracing spans
66+
- Automatically skips tracing 401/403/404 responses to reduce noise
67+
- Only active if SENTRY_DSN is configured
68+
69+
**Error Handling:**
70+
- `withOnError()` - Automatically captures 5xx errors and exceptions to Sentry
71+
- Adds context for HTTP exceptions
72+
- Handles AggregateError by capturing each error individually
73+
- Only sends to Sentry if SENTRY_DSN is configured
74+
75+
**Configuration:**
76+
- `SENTRY_DSN` (optional) - Sentry project DSN, Sentry only activates if provided
77+
- `SENTRY_RELEASE` (required) - Release version, automatically set to git commit hash during deployment
78+
- `ENVIRONMENT` (required) - Environment name (development/staging/production)
79+
80+
All Sentry functionality is opt-in via the SENTRY_DSN environment variable.
81+
</sentry-integration>
82+
4883
<code-style>
4984
- Use tabs for indentation, spaces for alignment
5085
- Type imports use `import type`

packages/hono-helpers/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
},
1313
"dependencies": {
1414
"@hono/standard-validator": "0.1.5",
15+
"@sentry/cloudflare": "^10.23.0",
16+
"@sentry/core": "^10.23.0",
1517
"hono": "4.9.11",
1618
"http-codex": "0.6.2",
1719
"workers-tagged-logger": "0.13.3",
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
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+
}

packages/hono-helpers/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ export { logger } from './helpers/logger'
33
export { getRequestLogData, type LogDataRequest } from './helpers/request'
44
export * from './helpers/errors'
55
export * from './helpers/url'
6+
export * from './helpers/instrumentation'
67
export * from './middleware/withCache'
78
export * from './middleware/withDefaultCors'
89
export * from './middleware/withNotFound'
910
export * from './middleware/withOnError'
11+
export * from './middleware/withSentry'

packages/hono-helpers/src/middleware/withOnError.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as Sentry from '@sentry/cloudflare'
12
import { HTTPException } from 'hono/http-exception'
23
import { httpStatus } from 'http-codex/status'
34

@@ -17,8 +18,14 @@ export function withOnError<T extends HonoApp>() {
1718
const status = err.getResponse().status as ContentfulStatusCode
1819
const body: APIError = { success: false, error: { message: err.message } }
1920
if (status >= 500) {
20-
// TODO: Capture to Sentry
21-
// Log to Sentry
21+
// Capture 5xx errors to Sentry with context
22+
Sentry.withScope((scope) => {
23+
scope.setContext('HTTP Exception', {
24+
status: status,
25+
body,
26+
})
27+
Sentry.captureException(err)
28+
})
2229
logger.error(err)
2330
} else if (status === httpStatus.Unauthorized) {
2431
body.error.message = 'unauthorized'
@@ -27,7 +34,8 @@ export function withOnError<T extends HonoApp>() {
2734
return c.json(body, status)
2835
}
2936

30-
// TODO: Capture to Sentry
37+
// Capture all other error types to Sentry
38+
Sentry.captureException(err)
3139
logger.error(err)
3240
return c.json(
3341
{

0 commit comments

Comments
 (0)