66 createBase64DecoderTransformStream ,
77 createBase64EncoderTransformStream ,
88} from 'cloudflare-internal:streaming-base64' ;
9+ import { withSpan , type Span } from 'cloudflare-internal:tracing-helpers' ;
910
1011type Fetcher = {
1112 fetch : typeof fetch ;
@@ -121,40 +122,51 @@ class ImageTransformerImpl implements ImageTransformer {
121122 async output (
122123 options : ImageOutputOptions
123124 ) : Promise < ImageTransformationResult > {
124- const formData = new StreamableFormData ( ) ;
125+ return await withSpan ( 'images_output' , async ( span ) => {
126+ span . setAttribute ( 'cloudflare.binding.type' , 'Images' ) ;
127+ const formData = new StreamableFormData ( ) ;
125128
126- this . #consume( ) ;
127- formData . append ( 'image' , this . #stream, { type : 'file' } ) ;
129+ this . #consume( ) ;
130+ formData . append ( 'image' , this . #stream, { type : 'file' } ) ;
128131
129- this . #serializeTransforms( formData ) ;
132+ this . #serializeTransforms( formData , span ) ;
130133
131- formData . append ( 'output_format' , options . format ) ;
132- if ( options . quality !== undefined ) {
133- formData . append ( 'output_quality' , options . quality . toString ( ) ) ;
134- }
134+ span . setAttribute ( 'cloudflare.images.options.format' , options . format ) ;
135+ formData . append ( 'output_format' , options . format ) ;
135136
136- if ( options . background !== undefined ) {
137- formData . append ( 'background' , options . background ) ;
138- }
137+ if ( options . quality !== undefined ) {
138+ span . setAttribute ( 'cloudflare.images.options.quality' , options . quality ) ;
139+ formData . append ( 'output_quality' , options . quality . toString ( ) ) ;
140+ }
139141
140- if ( options . anim !== undefined ) {
141- formData . append ( 'anim' , options . anim . toString ( ) ) ;
142- }
142+ if ( options . background !== undefined ) {
143+ span . setAttribute (
144+ 'cloudflare.images.options.background' ,
145+ options . background
146+ ) ;
147+ formData . append ( 'background' , options . background ) ;
148+ }
143149
144- const response = await this . #fetcher. fetch (
145- 'https://js.images.cloudflare.com/transform' ,
146- {
147- method : 'POST' ,
148- headers : {
149- 'content-type' : formData . contentType ( ) ,
150- } ,
151- body : formData . stream ( ) ,
150+ if ( options . anim !== undefined ) {
151+ span . setAttribute ( 'cloudflare.images.options.anim' , options . anim ) ;
152+ formData . append ( 'anim' , options . anim . toString ( ) ) ;
152153 }
153- ) ;
154154
155- await throwErrorIfErrorResponse ( 'TRANSFORM' , response ) ;
155+ const response = await this . #fetcher. fetch (
156+ 'https://js.images.cloudflare.com/transform' ,
157+ {
158+ method : 'POST' ,
159+ headers : {
160+ 'content-type' : formData . contentType ( ) ,
161+ } ,
162+ body : formData . stream ( ) ,
163+ }
164+ ) ;
165+
166+ await throwErrorIfErrorResponse ( 'TRANSFORM' , response , span ) ;
156167
157- return new TransformationResultImpl ( response ) ;
168+ return new TransformationResultImpl ( response ) ;
169+ } ) ;
158170 }
159171
160172 #consume( ) : void {
@@ -168,7 +180,7 @@ class ImageTransformerImpl implements ImageTransformer {
168180 this . #consumed = true ;
169181 }
170182
171- #serializeTransforms( formData : StreamableFormData ) : void {
183+ #serializeTransforms( formData : StreamableFormData , span : Span ) : void {
172184 const transforms : ( TargetedTransform | DrawCommand ) [ ] = [ ] ;
173185
174186 // image 0 is the canvas, so the first draw_image has index 1
@@ -210,6 +222,16 @@ class ImageTransformerImpl implements ImageTransformer {
210222 }
211223
212224 walkTransforms ( 0 , this . #transforms) ;
225+
226+ // The transforms are a set of operations which are applied to the image in order.
227+ // Attaching an attribute as JSON is a little odd, but I'm not sure if there is
228+ // a better way to do this.
229+ if ( transforms . length > 0 ) {
230+ span . setAttribute (
231+ 'cloudflare.images.options.transforms' ,
232+ JSON . stringify ( transforms )
233+ ) ;
234+ }
213235 formData . append ( 'transforms' , JSON . stringify ( transforms ) ) ;
214236 }
215237}
@@ -233,40 +255,53 @@ class ImagesBindingImpl implements ImagesBinding {
233255 stream : ReadableStream < Uint8Array > ,
234256 options ?: ImageInputOptions
235257 ) : Promise < ImageInfoResponse > {
236- const body = new StreamableFormData ( ) ;
258+ return await withSpan ( 'images_info' , async ( span ) => {
259+ span . setAttribute ( 'cloudflare.binding.type' , 'Images' ) ;
260+ const body = new StreamableFormData ( ) ;
237261
238- const decodedStream =
239- options ?. encoding === 'base64'
240- ? stream . pipeThrough ( createBase64DecoderTransformStream ( ) )
241- : stream ;
242-
243- body . append ( 'image' , decodedStream , { type : 'file' } ) ;
262+ span . setAttribute (
263+ 'cloudflare.images.options.encoding' ,
264+ options ?. encoding ?? 'base64'
265+ ) ;
266+ const decodedStream =
267+ options ?. encoding === 'base64'
268+ ? stream . pipeThrough ( createBase64DecoderTransformStream ( ) )
269+ : stream ;
270+
271+ body . append ( 'image' , decodedStream , { type : 'file' } ) ;
272+
273+ const response = await this . #fetcher. fetch (
274+ 'https://js.images.cloudflare.com/info' ,
275+ {
276+ method : 'POST' ,
277+ headers : {
278+ 'content-type' : body . contentType ( ) ,
279+ } ,
280+ body : body . stream ( ) ,
281+ }
282+ ) ;
244283
245- const response = await this . #fetcher. fetch (
246- 'https://js.images.cloudflare.com/info' ,
247- {
248- method : 'POST' ,
249- headers : {
250- 'content-type' : body . contentType ( ) ,
251- } ,
252- body : body . stream ( ) ,
253- }
254- ) ;
284+ await throwErrorIfErrorResponse ( 'INFO' , response , span ) ;
255285
256- await throwErrorIfErrorResponse ( 'INFO' , response ) ;
286+ const r = ( await response . json ( ) ) as RawInfoResponse ;
257287
258- const r = ( await response . json ( ) ) as RawInfoResponse ;
288+ span . setAttribute ( 'cloudflare.images.result.format' , r . format ) ;
259289
260- if ( 'file_size' in r ) {
261- return {
262- fileSize : r . file_size ,
263- width : r . width ,
264- height : r . height ,
265- format : r . format ,
266- } ;
267- }
290+ if ( 'file_size' in r ) {
291+ const ret = {
292+ fileSize : r . file_size ,
293+ width : r . width ,
294+ height : r . height ,
295+ format : r . format ,
296+ } ;
297+ span . setAttribute ( 'cloudflare.images.result.file_size' , ret . fileSize ) ;
298+ span . setAttribute ( 'cloudflare.images.result.width' , ret . width ) ;
299+ span . setAttribute ( 'cloudflare.images.result.height' , ret . height ) ;
300+ return ret ;
301+ }
268302
269- return r ;
303+ return r ;
304+ } ) ;
270305 }
271306
272307 input (
@@ -292,24 +327,29 @@ class ImagesErrorImpl extends Error implements ImagesError {
292327
293328async function throwErrorIfErrorResponse (
294329 operation : string ,
295- response : Response
330+ response : Response ,
331+ span : Span
296332) : Promise < void > {
297333 const statusHeader = response . headers . get ( 'cf-images-binding' ) || '' ;
298334
299335 const match = / e r r = ( \d + ) / . exec ( statusHeader ) ;
300336
301337 if ( match && match [ 1 ] ) {
338+ const errorMessage = await response . text ( ) ;
339+ span . setAttribute ( 'cloudflare.images.error.code' , match [ 1 ] ) ;
340+ span . setAttribute ( 'error.type' , errorMessage ) ;
302341 throw new ImagesErrorImpl (
303- `IMAGES_${ operation } _${ await response . text ( ) } ` . trim ( ) ,
342+ `IMAGES_${ operation } _${ errorMessage } ` . trim ( ) ,
304343 Number . parseInt ( match [ 1 ] )
305344 ) ;
306345 }
307346
308347 if ( response . status > 399 ) {
348+ const errorMessage = await response . text ( ) ;
349+ span . setAttribute ( 'cloudflare.images.error.code' , '9523' ) ;
350+ span . setAttribute ( 'error.type' , errorMessage ) ;
309351 throw new ImagesErrorImpl (
310- `Unexpected error response ${ response . status } : ${ (
311- await response . text ( )
312- ) . trim ( ) } `,
352+ `Unexpected error response ${ response . status } : ${ errorMessage . trim ( ) } ` ,
313353 9523
314354 ) ;
315355 }
0 commit comments