@@ -4,12 +4,122 @@ import {
44} from '@opentelemetry/instrumentation-fetch' ;
55
66import { captureTraceParent } from './servertiming' ;
7- import { headerCapture } from './utils' ;
7+ import { RedactableKey , headerCapture , shouldRedactKey } from './utils' ;
88
99export type HyperDXFetchInstrumentationConfig = FetchInstrumentationConfig & {
1010 advancedNetworkCapture ?: ( ) => boolean ;
11+ redactKeys ?: {
12+ headers ?: RedactableKey [ ] ;
13+ body ?: RedactableKey [ ] ;
14+ } ;
1115} ;
1216
17+ function redactValue ( ) : string {
18+ return '[REDACTED]' ;
19+ }
20+
21+ function redactObject ( obj : any , redactConfig : RedactableKey [ ] | undefined ) {
22+ if ( ! redactConfig || ! obj || typeof obj !== 'object' ) {
23+ return obj ;
24+ }
25+
26+ if ( Array . isArray ( obj ) ) {
27+ return obj . map ( ( item ) => redactObject ( item , redactConfig ) ) ;
28+ }
29+
30+ const result : any = { } ;
31+ for ( const [ key , value ] of Object . entries ( obj ) ) {
32+ if ( shouldRedactKey ( key , redactConfig ) ) {
33+ result [ key ] = redactValue ( ) ;
34+ } else if ( value && typeof value === 'object' ) {
35+ result [ key ] = redactObject ( value , redactConfig ) ;
36+ } else {
37+ result [ key ] = value ;
38+ }
39+ }
40+ return result ;
41+ }
42+
43+ function redactFormData (
44+ formData : FormData ,
45+ redactConfig : RedactableKey [ ] | undefined ,
46+ ) : string {
47+ if ( ! redactConfig ) {
48+ return formData . toString ( ) ;
49+ }
50+
51+ const entries : Array < [ string , string ] > = [ ] ;
52+ formData . forEach ( ( value , key ) => {
53+ if ( shouldRedactKey ( key , redactConfig ) ) {
54+ entries . push ( [ key , redactValue ( ) ] ) ;
55+ } else {
56+ entries . push ( [ key , value . toString ( ) ] ) ;
57+ }
58+ } ) ;
59+
60+ return JSON . stringify ( Object . fromEntries ( entries ) ) ;
61+ }
62+
63+ function redactURLSearchParams (
64+ params : URLSearchParams ,
65+ redactConfig : RedactableKey [ ] | undefined ,
66+ ) : string {
67+ if ( ! redactConfig ) {
68+ return params . toString ( ) ;
69+ }
70+
71+ const newParams = new URLSearchParams ( ) ;
72+ params . forEach ( ( value , key ) => {
73+ if ( shouldRedactKey ( key , redactConfig ) ) {
74+ newParams . set ( key , redactValue ( ) ) ;
75+ } else {
76+ newParams . set ( key , value ) ;
77+ }
78+ } ) ;
79+
80+ return newParams . toString ( ) ;
81+ }
82+
83+ function redactRequestBody (
84+ body : ReadableStream < Uint8Array > | BodyInit ,
85+ redactConfig : RedactableKey [ ] | undefined ,
86+ ) : string {
87+ if ( ! body ) return '' ;
88+
89+ // Maintain backward compatibility with ReadableStream
90+ if ( body instanceof ReadableStream ) {
91+ return '[ReadableStream]' ;
92+ }
93+
94+ if ( typeof FormData !== 'undefined' && body instanceof FormData ) {
95+ return redactFormData ( body , redactConfig ) ;
96+ }
97+
98+ if (
99+ typeof URLSearchParams !== 'undefined' &&
100+ body instanceof URLSearchParams
101+ ) {
102+ return redactURLSearchParams ( body , redactConfig ) ;
103+ }
104+
105+ if ( typeof body === 'string' ) {
106+ if ( ! redactConfig ) {
107+ return body ;
108+ }
109+
110+ try {
111+ const parsed = JSON . parse ( body ) ;
112+ const redacted = redactObject ( parsed , redactConfig ) ;
113+ return JSON . stringify ( redacted ) ;
114+ } catch {
115+ // Not JSON, return as-is
116+ return body ;
117+ }
118+ }
119+
120+ return body . toString ( ) ;
121+ }
122+
13123// not used yet
14124async function readStream ( stream : ReadableStream ) : Promise < string > {
15125 const chunks : string [ ] = [ ] ;
@@ -43,21 +153,18 @@ export class HyperDXFetchInstrumentation extends FetchInstrumentation {
43153
44154 if ( config . advancedNetworkCapture ?.( ) && span ) {
45155 if ( request . headers ) {
46- headerCapture ( 'request' , Object . keys ( request . headers ) ) (
47- span ,
48- ( header ) => request . headers ?. [ header ] ,
49- ) ;
156+ headerCapture (
157+ 'request' ,
158+ Object . keys ( request . headers ) ,
159+ config . redactKeys ?. headers ,
160+ ) ( span , ( header ) => request . headers ?. [ header ] ) ;
50161 }
51162 if ( request . body ) {
52- if ( request . body instanceof ReadableStream ) {
53- span . setAttribute ( 'http.request.body' , '[ReadableStream]' ) ;
54- // FIXME: This is not working yet
55- // readStream(request.body).then((body) => {
56- // span.setAttribute('http.request.body', body);
57- // });
58- } else {
59- span . setAttribute ( 'http.request.body' , request . body . toString ( ) ) ;
60- }
163+ const redactedBody = redactRequestBody (
164+ request . body ,
165+ config . redactKeys ?. body ,
166+ ) ;
167+ span . setAttribute ( 'http.request.body' , redactedBody ) ;
61168 }
62169
63170 if ( response instanceof Response ) {
@@ -66,15 +173,17 @@ export class HyperDXFetchInstrumentation extends FetchInstrumentation {
66173 response . headers . forEach ( ( value , name ) => {
67174 headerNames . push ( name ) ;
68175 } ) ;
69- headerCapture ( 'response' , headerNames ) (
70- span ,
71- ( header ) => response . headers . get ( header ) ?? '' ,
72- ) ;
176+ headerCapture (
177+ 'response' ,
178+ headerNames ,
179+ config . redactKeys ?. headers ,
180+ ) ( span , ( header ) => response . headers . get ( header ) ?? '' ) ;
73181 }
74182 response
75183 . clone ( )
76184 . text ( )
77185 . then ( ( body ) => {
186+ // TODO: redact response body
78187 span . setAttribute ( 'http.response.body' , body ) ;
79188 } )
80189 . catch ( ( ) => {
0 commit comments