@@ -49,16 +49,18 @@ export class VercelSandboxPipelineHost {
4949 // be explicit false for one-shot deposit/read hosts (Snapshot Storage
5050 // is billed separately). normalizeCreateOptions enforces that + a unique name.
5151 const createOptions = normalizeCreateOptions ( plan . createOptions ) ;
52+ const createStartedAt = new Date ( ) . toISOString ( ) ;
5253 await this . emit ( {
5354 type : 'sandbox-create-started' ,
54- timestamp : new Date ( ) . toISOString ( ) ,
55+ timestamp : createStartedAt ,
5556 runtime : createOptions . runtime ,
56- // Source-safe: image ref only (no tokens). Helps diagnose create 400s.
5757 image : createOptions . image ?? null ,
5858 mode : plan . manifest . hostMode ,
5959 hasSource : Boolean ( createOptions . source ) ,
6060 persistent : createOptions . persistent === true ,
61- } as PipelineHostEvent ) ;
61+ name : createOptions . name ,
62+ timeoutMs : createOptions . timeout ,
63+ } ) ;
6264 let sandbox : SandboxSession ;
6365 try {
6466 sandbox = await withTimeout (
@@ -67,7 +69,20 @@ export class VercelSandboxPipelineHost {
6769 `Vercel Sandbox create did not complete within ${ this . sandboxCreateTimeoutMs } ms.` ,
6870 ) ;
6971 } catch ( error ) {
70- throw new Error ( formatSandboxApiError ( error , 'Sandbox.create' ) , { cause : error } ) ;
72+ const message = formatSandboxApiError ( error , 'Sandbox.create' ) ;
73+ const httpStatus = extractSandboxHttpStatus ( error ) ;
74+ await this . emit ( {
75+ type : 'sandbox-create-failed' ,
76+ timestamp : new Date ( ) . toISOString ( ) ,
77+ mode : plan . manifest . hostMode ,
78+ image : createOptions . image ?? null ,
79+ runtime : createOptions . runtime ,
80+ hasSource : Boolean ( createOptions . source ) ,
81+ name : createOptions . name ,
82+ message,
83+ httpStatus,
84+ } ) ;
85+ throw new Error ( message , { cause : error } ) ;
7186 }
7287 const sandboxIdentity = resolveSandboxIdentity ( sandbox , createOptions . name ) ;
7388 await this . emit ( {
@@ -77,6 +92,7 @@ export class VercelSandboxPipelineHost {
7792 name : sandboxIdentity . name ,
7893 persistent : createOptions . persistent === true ,
7994 status : sandbox . status ,
95+ image : createOptions . image ?? null ,
8096 } ) ;
8197 const commands : PipelineHostCommandResult [ ] = [ ] ;
8298 let stopped = false ;
@@ -481,60 +497,127 @@ export function normalizeCreateOptions(
481497 } ;
482498}
483499
500+ type SandboxErrorShape = Error & {
501+ json ?: unknown ;
502+ text ?: string ;
503+ response ?: { status ?: number ; statusText ?: string } ;
504+ sandboxName ?: string ;
505+ cause ?: unknown ;
506+ } ;
507+
508+ function collectErrorChain ( error : unknown , depth = 0 ) : SandboxErrorShape [ ] {
509+ if ( depth > 5 || error == null ) return [ ] ;
510+ if ( ! ( error instanceof Error ) ) return [ ] ;
511+ const chain = [ error as SandboxErrorShape ] ;
512+ const cause = ( error as SandboxErrorShape ) . cause ;
513+ if ( cause ) chain . push ( ...collectErrorChain ( cause , depth + 1 ) ) ;
514+ return chain ;
515+ }
516+
517+ function detailFromJsonBody ( body : Record < string , unknown > ) : string {
518+ const msg =
519+ ( typeof body . message === 'string' && body . message ) ||
520+ ( typeof body . error === 'string' && body . error ) ||
521+ ( body . error &&
522+ typeof body . error === 'object' &&
523+ typeof ( body . error as { message ?: string } ) . message === 'string' &&
524+ ( body . error as { message : string } ) . message ) ||
525+ null ;
526+ const code =
527+ ( typeof body . code === 'string' && body . code ) ||
528+ ( body . error &&
529+ typeof body . error === 'object' &&
530+ typeof ( body . error as { code ?: string } ) . code === 'string' &&
531+ ( body . error as { code : string } ) . code ) ||
532+ null ;
533+ const joined = [ code , msg ] . filter ( Boolean ) . join ( ': ' ) ;
534+ if ( joined ) return joined ;
535+ try {
536+ return JSON . stringify ( body ) . slice ( 0 , 500 ) ;
537+ } catch {
538+ return '' ;
539+ }
540+ }
541+
484542/**
485543 * Expand Vercel Sandbox SDK APIError ("Status code 400 is not ok") with response
486544 * body fields so Production logs / UI can show the real reject reason.
545+ * Walks `error.cause` so wrappers still expose the SDK body.
487546 */
488547export function formatSandboxApiError ( error : unknown , phase : string ) : string {
489548 if ( ! ( error instanceof Error ) ) {
490549 return `${ phase } failed: ${ String ( error ) } ` ;
491550 }
492- const anyErr = error as Error & {
493- json ?: unknown ;
494- text ?: string ;
495- response ?: { status ?: number ; statusText ?: string } ;
496- sandboxName ?: string ;
497- } ;
498- const status = anyErr . response ?. status ;
499- const statusText = anyErr . response ?. statusText ;
551+ const chain = collectErrorChain ( error ) ;
552+ let status : number | undefined ;
553+ let statusText : string | undefined ;
500554 let detail = '' ;
501- if ( anyErr . json && typeof anyErr . json === 'object' ) {
502- const body = anyErr . json as Record < string , unknown > ;
503- const msg =
504- ( typeof body . message === 'string' && body . message ) ||
505- ( typeof body . error === 'string' && body . error ) ||
506- ( body . error &&
507- typeof body . error === 'object' &&
508- typeof ( body . error as { message ?: string } ) . message === 'string' &&
509- ( body . error as { message : string } ) . message ) ||
510- null ;
511- const code =
512- ( typeof body . code === 'string' && body . code ) ||
513- ( body . error &&
514- typeof body . error === 'object' &&
515- typeof ( body . error as { code ?: string } ) . code === 'string' &&
516- ( body . error as { code : string } ) . code ) ||
517- null ;
518- detail = [ code , msg ] . filter ( Boolean ) . join ( ': ' ) ;
519- if ( ! detail ) {
520- try {
521- detail = JSON . stringify ( body ) . slice ( 0 , 400 ) ;
522- } catch {
523- detail = '' ;
524- }
555+ const messages : string [ ] = [ ] ;
556+
557+ for ( const entry of chain ) {
558+ if ( entry . message ?. trim ( ) ) messages . push ( entry . message . trim ( ) ) ;
559+ if ( typeof entry . response ?. status === 'number' ) {
560+ status = entry . response . status ;
561+ statusText = entry . response . statusText ;
562+ }
563+ if ( ! detail && entry . json && typeof entry . json === 'object' ) {
564+ detail = detailFromJsonBody ( entry . json as Record < string , unknown > ) ;
565+ }
566+ if ( ! detail && typeof entry . text === 'string' && entry . text . trim ( ) ) {
567+ detail = entry . text . trim ( ) . slice ( 0 , 500 ) ;
525568 }
526- } else if ( typeof anyErr . text === 'string' && anyErr . text . trim ( ) ) {
527- detail = anyErr . text . trim ( ) . slice ( 0 , 400 ) ;
528569 }
529- const base = error . message ?. trim ( ) || 'unknown error' ;
570+
571+ const base =
572+ messages . find ( ( m ) => ! / ^ S t a t u s c o d e \d + i s n o t o k $ / . test ( m ) ) ||
573+ messages [ 0 ] ||
574+ 'unknown error' ;
530575 const statusPart =
531576 typeof status === 'number' ? `HTTP ${ status } ${ statusText ? ` ${ statusText } ` : '' } ` : null ;
532- const parts = [ `${ phase } failed` , statusPart , base !== `Status code ${ status } is not ok` ? base : null , detail ]
577+ const parts = [ `${ phase } failed` , statusPart , base , detail ]
533578 . filter ( Boolean )
534579 . filter ( ( part , index , arr ) => arr . indexOf ( part ) === index ) ;
535580 return parts . join ( ' — ' ) ;
536581}
537582
583+ export function extractSandboxHttpStatus ( error : unknown ) : number | null {
584+ for ( const entry of collectErrorChain ( error ) ) {
585+ if ( typeof entry . response ?. status === 'number' ) return entry . response . status ;
586+ }
587+ return null ;
588+ }
589+
590+ /** Source-safe summary of create options for always-on server logs. */
591+ export function summarizeSandboxCreateOptions (
592+ createOptions : PipelineHostPlan [ 'createOptions' ] ,
593+ ) : Record < string , unknown > {
594+ const source = createOptions . source ;
595+ return {
596+ name : createOptions . name ?? null ,
597+ image : createOptions . image ?? null ,
598+ runtime : createOptions . runtime ?? null ,
599+ persistent : createOptions . persistent === true ,
600+ timeoutMs : createOptions . timeout ?? null ,
601+ hasToken : Boolean ( createOptions . token ) ,
602+ hasTeamId : Boolean ( createOptions . teamId ) ,
603+ hasProjectId : Boolean ( createOptions . projectId ) ,
604+ networkPolicy :
605+ typeof createOptions . networkPolicy === 'string'
606+ ? createOptions . networkPolicy
607+ : createOptions . networkPolicy
608+ ? 'custom'
609+ : null ,
610+ sourceType : source && typeof source === 'object' && 'type' in source ? source . type : null ,
611+ sourceHasAuth : Boolean (
612+ source &&
613+ typeof source === 'object' &&
614+ 'password' in source &&
615+ ( source as { password ?: string } ) . password ,
616+ ) ,
617+ envKeyCount : createOptions . env ? Object . keys ( createOptions . env ) . length : 0 ,
618+ } ;
619+ }
620+
538621function resolveSandboxIdentity (
539622 sandbox : SandboxSession ,
540623 fallbackName ?: string ,
0 commit comments