1111// ---------------------------------------------------------------------------
1212
1313import * as Sentry from "@sentry/cloudflare" ;
14+ import type { ErrorEvent , Scope } from "@sentry/cloudflare" ;
1415import { Cause , Effect , Layer } from "effect" ;
16+ import type * as Tracer from "effect/Tracer" ;
1517
1618import { ErrorCapture } from "@executor-js/api" ;
1719
@@ -21,12 +23,106 @@ import { ErrorCapture } from "@executor-js/api";
2123// error. Sentry still receives the full, untruncated cause via
2224// `setExtra`; only the dev-console mirror is capped.
2325const MAX_CONSOLE_CAUSE_CHARS = 4_000 ;
26+ const OTEL_TRACE_ID_PATTERN = / ^ [ 0 - 9 a - f ] { 32 } $ / ;
27+ const OTEL_SPAN_ID_PATTERN = / ^ [ 0 - 9 a - f ] { 16 } $ / ;
28+
29+ export const OTEL_TRACE_ID_TAG = "otel_trace_id" ;
30+ export const OTEL_SPAN_ID_TAG = "otel_span_id" ;
31+ export const SENTRY_EVENT_ID_ATTRIBUTE = "sentry.event_id" ;
32+
33+ export type OtelCorrelationContext = {
34+ readonly traceId : string ;
35+ readonly spanId : string ;
36+ } ;
2437
2538const truncate = ( s : string ) : string =>
2639 s . length <= MAX_CONSOLE_CAUSE_CHARS
2740 ? s
2841 : `${ s . slice ( 0 , MAX_CONSOLE_CAUSE_CHARS ) } \n…[truncated ${ s . length - MAX_CONSOLE_CAUSE_CHARS } chars]` ;
2942
43+ const validOtelContext = ( context : OtelCorrelationContext ) : boolean =>
44+ OTEL_TRACE_ID_PATTERN . test ( context . traceId ) && OTEL_SPAN_ID_PATTERN . test ( context . spanId ) ;
45+
46+ export const otelCorrelationContextFromEffectSpan = (
47+ span : Tracer . Span ,
48+ ) : OtelCorrelationContext | null => {
49+ const context = { traceId : span . traceId , spanId : span . spanId } ;
50+ return validOtelContext ( context ) ? context : null ;
51+ } ;
52+
53+ export const otelCorrelationContextFromOpenTelemetrySpan = ( span : {
54+ readonly spanContext : ( ) => { readonly traceId : string ; readonly spanId : string } ;
55+ } ) : OtelCorrelationContext | null => {
56+ const { traceId, spanId } = span . spanContext ( ) ;
57+ const context = { traceId, spanId } ;
58+ return validOtelContext ( context ) ? context : null ;
59+ } ;
60+
61+ export const addOtelCorrelationTags = < T extends { readonly tags ?: Record < string , unknown > } > (
62+ event : T ,
63+ context : OtelCorrelationContext | null ,
64+ ) : T => {
65+ if ( ! context ) return event ;
66+ return {
67+ ...event ,
68+ tags : {
69+ ...event . tags ,
70+ [ OTEL_TRACE_ID_TAG ] : context . traceId ,
71+ [ OTEL_SPAN_ID_TAG ] : context . spanId ,
72+ } ,
73+ } ;
74+ } ;
75+
76+ export const tagSentryScopeWithOtelContext = (
77+ scope : Scope ,
78+ context : OtelCorrelationContext | null ,
79+ ) : void => {
80+ if ( ! context ) return ;
81+ scope . setTag ( OTEL_TRACE_ID_TAG , context . traceId ) ;
82+ scope . setTag ( OTEL_SPAN_ID_TAG , context . spanId ) ;
83+ } ;
84+
85+ export const tagCurrentSentryScopeWithOtelContext = (
86+ context : OtelCorrelationContext | null ,
87+ ) : void => {
88+ tagSentryScopeWithOtelContext ( Sentry . getCurrentScope ( ) , context ) ;
89+ } ;
90+
91+ const currentOtelContext = Effect . map (
92+ Effect . currentSpan ,
93+ otelCorrelationContextFromEffectSpan ,
94+ ) . pipe ( Effect . orElseSucceed ( ( ) => null ) ) ;
95+
96+ export const tagCurrentSentryScopeWithCurrentOtelSpan : Effect . Effect < OtelCorrelationContext | null > =
97+ Effect . map ( currentOtelContext , ( context ) => {
98+ tagCurrentSentryScopeWithOtelContext ( context ) ;
99+ return context ;
100+ } ) ;
101+
102+ export const beforeSendWithOtelCorrelation = (
103+ event : ErrorEvent ,
104+ options ?: { readonly logPayload ?: boolean } ,
105+ ) : ErrorEvent => {
106+ if ( options ?. logPayload ) {
107+ console . info (
108+ JSON . stringify ( {
109+ event : "sentry_before_send_otel_correlation" ,
110+ sentry_event_id : event . event_id ?? "" ,
111+ otel_trace_id : String ( event . tags ?. [ OTEL_TRACE_ID_TAG ] ?? "" ) ,
112+ otel_span_id : String ( event . tags ?. [ OTEL_SPAN_ID_TAG ] ?? "" ) ,
113+ } ) ,
114+ ) ;
115+ }
116+ return event ;
117+ } ;
118+
119+ export const addCurrentOtelCorrelationTags = <
120+ T extends { readonly tags ?: Record < string , unknown > } ,
121+ > (
122+ event : T ,
123+ ) : Effect . Effect < T > =>
124+ Effect . map ( currentOtelContext , ( context ) => addOtelCorrelationTags ( event , context ) ) ;
125+
30126// Sentry's `captureException` can't serialize Effect's `CauseImpl` (it logs
31127// `'CauseImpl' captured as exception with keys: reasons, ~effect/Cause` and
32128// drops the real failure). `Cause.squash` isn't enough on its own: when an
@@ -48,21 +144,36 @@ export const sentryPayloadForCause = (
48144 return { primary : input , pretty : null } ;
49145} ;
50146
51- export const captureCause = ( input : unknown ) : string | undefined => {
147+ export const captureCause = (
148+ input : unknown ,
149+ context : OtelCorrelationContext | null = null ,
150+ ) : string | undefined => {
52151 const { primary, pretty } = sentryPayloadForCause ( input ) ;
152+ tagCurrentSentryScopeWithOtelContext ( context ) ;
53153 return Sentry . captureException ( primary , ( scope ) => {
154+ tagSentryScopeWithOtelContext ( scope , context ) ;
54155 if ( pretty !== null ) scope . setExtra ( "cause" , pretty ) ;
55156 return scope ;
56157 } ) ;
57158} ;
58159
160+ export const captureCauseEffect = ( input : unknown ) : Effect . Effect < string | undefined > =>
161+ Effect . gen ( function * ( ) {
162+ const context = yield * tagCurrentSentryScopeWithCurrentOtelSpan ;
163+ const eventId = yield * Effect . sync ( ( ) => captureCause ( input , context ) ) ;
164+ if ( eventId && context ) {
165+ yield * Effect . annotateCurrentSpan ( SENTRY_EVENT_ID_ATTRIBUTE , eventId ) ;
166+ }
167+ return eventId ;
168+ } ) ;
169+
59170export const ErrorCaptureLive : Layer . Layer < ErrorCapture > = Layer . succeed (
60171 ErrorCapture ,
61172 ErrorCapture . of ( {
62173 captureException : ( cause ) =>
63- Effect . sync ( ( ) => {
174+ Effect . gen ( function * ( ) {
64175 console . error ( "[api] unhandled cause:" , truncate ( Cause . pretty ( cause ) ) ) ;
65- return captureCause ( cause ) ?? "" ;
176+ return ( yield * captureCauseEffect ( cause ) ) ?? "" ;
66177 } ) ,
67178 } ) ,
68179) ;
0 commit comments