33 */
44
55import { ApiSpec , HttpRoute , KeyToHttpStatus } from '@api-ts/io-ts-http' ;
6+ import type { Span } from '@opentelemetry/api' ;
67import express from 'express' ;
78import * as E from 'fp-ts/Either' ;
89import { pipe } from 'fp-ts/pipeable' ;
9- import { defaultOnDecodeError , defaultOnEncodeError } from './errors' ;
10+
11+ import {
12+ defaultDecodeErrorFormatter ,
13+ defaultEncodeErrorFormatter ,
14+ defaultGetDecodeErrorStatusCode ,
15+ defaultGetEncodeErrorStatusCode ,
16+ } from './errors' ;
1017import { apiTsPathToExpress } from './path' ;
18+ import {
19+ ApiTsAttributes ,
20+ createDecodeSpan ,
21+ createSendEncodedSpan ,
22+ endSpan ,
23+ recordSpanDecodeError ,
24+ recordSpanEncodeError ,
25+ setSpanAttributes ,
26+ } from './telemetry' ;
1127import {
1228 AddRouteHandler ,
1329 AddUncheckedRouteHandler ,
@@ -22,8 +38,10 @@ import {
2238
2339export type {
2440 AfterEncodedResponseSentFn ,
25- OnDecodeErrorFn ,
26- OnEncodeErrorFn ,
41+ DecodeErrorFormatterFn ,
42+ EncodeErrorFormatterFn ,
43+ GetDecodeErrorStatusCodeFn ,
44+ GetEncodeErrorStatusCodeFn ,
2745 TypedRequestHandler ,
2846 UncheckedRequestHandler ,
2947 WrappedRouter ,
@@ -43,17 +61,21 @@ export type {
4361export function createRouter < Spec extends ApiSpec > (
4462 spec : Spec ,
4563 {
46- onDecodeError ,
47- onEncodeError ,
64+ encodeErrorFormatter ,
65+ getEncodeErrorStatusCode ,
4866 afterEncodedResponseSent,
67+ decodeErrorFormatter,
68+ getDecodeErrorStatusCode,
4969 ...options
5070 } : WrappedRouterOptions = { } ,
5171) : WrappedRouter < Spec > {
5272 const router = express . Router ( options ) ;
5373 return wrapRouter ( router , spec , {
54- onDecodeError ,
55- onEncodeError ,
74+ encodeErrorFormatter ,
75+ getEncodeErrorStatusCode ,
5676 afterEncodedResponseSent,
77+ decodeErrorFormatter,
78+ getDecodeErrorStatusCode,
5779 } ) ;
5880}
5981
@@ -69,9 +91,11 @@ export function wrapRouter<Spec extends ApiSpec>(
6991 router : express . Router ,
7092 spec : Spec ,
7193 {
72- onDecodeError = defaultOnDecodeError ,
73- onEncodeError = defaultOnEncodeError ,
94+ encodeErrorFormatter = defaultEncodeErrorFormatter ,
95+ getEncodeErrorStatusCode = defaultGetEncodeErrorStatusCode ,
7496 afterEncodedResponseSent = ( ) => { } ,
97+ decodeErrorFormatter = defaultDecodeErrorFormatter ,
98+ getDecodeErrorStatusCode = defaultGetDecodeErrorStatusCode ,
7599 } : WrappedRouteOptions ,
76100) : WrappedRouter < Spec > {
77101 const routerMiddleware : UncheckedRequestHandler [ ] = [ ] ;
@@ -81,12 +105,14 @@ export function wrapRouter<Spec extends ApiSpec>(
81105 ) : AddUncheckedRouteHandler < Spec , Method > {
82106 return ( apiName , handlers , options ) => {
83107 const route : HttpRoute | undefined = spec [ apiName ] ?. [ method ] ;
108+ let decodeSpan : Span | undefined ;
84109 if ( route === undefined ) {
85110 // Should only happen with an explicit undefined property, which we can only prevent at the
86111 // type level with the `exactOptionalPropertyTypes` tsconfig option
87112 throw Error ( `Method "${ method } " at "${ apiName } " must not be "undefined"'` ) ;
88113 }
89114 const wrapReqAndRes : UncheckedRequestHandler = ( req , res , next ) => {
115+ decodeSpan = createDecodeSpan ( { apiName, httpRoute : route } ) ;
90116 // Intentionally passing explicit arguments here instead of decoding
91117 // req by itself because of issues that arise while using Node 16
92118 // See https://github.com/BitGo/api-ts/pull/394 for more information.
@@ -103,6 +129,10 @@ export function wrapRouter<Spec extends ApiSpec>(
103129 status : keyof ( typeof route ) [ 'response' ] ,
104130 payload : unknown ,
105131 ) => {
132+ const encodeSpan = createSendEncodedSpan ( {
133+ apiName,
134+ httpRoute : route ,
135+ } ) ;
106136 try {
107137 const codec = route . response [ status ] ;
108138 if ( ! codec ) {
@@ -112,6 +142,9 @@ export function wrapRouter<Spec extends ApiSpec>(
112142 typeof status === 'number'
113143 ? status
114144 : KeyToHttpStatus [ status as keyof KeyToHttpStatus ] ;
145+ setSpanAttributes ( encodeSpan , {
146+ [ ApiTsAttributes . API_TS_STATUS_CODE ] : statusCode ,
147+ } ) ;
115148 if ( statusCode === undefined ) {
116149 throw new Error ( `unknown HTTP status code for key ${ status } ` ) ;
117150 } else if ( ! codec . is ( payload ) ) {
@@ -126,18 +159,42 @@ export function wrapRouter<Spec extends ApiSpec>(
126159 res as WrappedResponse ,
127160 ) ;
128161 } catch ( err ) {
129- ( options ?. onEncodeError ?? onEncodeError ) (
130- err ,
131- req as WrappedRequest ,
132- res as WrappedResponse ,
133- ) ;
162+ const statusCode = (
163+ options ?. getEncodeErrorStatusCode ?? getEncodeErrorStatusCode
164+ ) ( err , req ) ;
165+ const encodeErrorMessage = (
166+ options ?. encodeErrorFormatter ?? encodeErrorFormatter
167+ ) ( err , req ) ;
168+
169+ recordSpanEncodeError ( encodeSpan , err , statusCode ) ;
170+ res . status ( statusCode ) . json ( encodeErrorMessage ) ;
171+ } finally {
172+ endSpan ( encodeSpan ) ;
134173 }
135174 } ;
136175 next ( ) ;
137176 } ;
138177
178+ const endDecodeSpanMiddleware : UncheckedRequestHandler = ( req , _res , next ) => {
179+ pipe (
180+ req . decoded ,
181+ E . getOrElseW ( ( errs ) => {
182+ const decodeErrorMessage = (
183+ options ?. decodeErrorFormatter ?? decodeErrorFormatter
184+ ) ( errs , req ) ;
185+ const statusCode = (
186+ options ?. getDecodeErrorStatusCode ?? getDecodeErrorStatusCode
187+ ) ( errs , req ) ;
188+ recordSpanDecodeError ( decodeSpan , decodeErrorMessage , statusCode ) ;
189+ } ) ,
190+ ) ;
191+ endSpan ( decodeSpan ) ;
192+ next ( ) ;
193+ } ;
194+
139195 const middlewareChain = [
140196 wrapReqAndRes ,
197+ endDecodeSpanMiddleware ,
141198 ...routerMiddleware ,
142199 ...handlers ,
143200 ] as express . RequestHandler [ ] ;
@@ -164,7 +221,13 @@ export function wrapRouter<Spec extends ApiSpec>(
164221 req . decoded ,
165222 E . matchW (
166223 ( errs ) => {
167- ( options ?. onDecodeError ?? onDecodeError ) ( errs , req , res ) ;
224+ const statusCode = (
225+ options ?. getDecodeErrorStatusCode ?? getDecodeErrorStatusCode
226+ ) ( errs , req ) ;
227+ const decodeErrorMessage = (
228+ options ?. decodeErrorFormatter ?? decodeErrorFormatter
229+ ) ( errs , req ) ;
230+ res . status ( statusCode ) . json ( decodeErrorMessage ) ;
168231 } ,
169232 ( value ) => {
170233 req . decoded = value ;
0 commit comments