@@ -357,7 +357,7 @@ static zend_ulong ddtrace_compute_exception_hash(zend_object *exception) {
357357 return hash ;
358358}
359359
360- static void ddtrace_collect_exception_debug_data (zend_object * exception , zend_string * service_name , uint64_t time , ddog_SpanBytes * span ) {
360+ static void ddtrace_collect_exception_debug_data (zend_object * exception , zend_object * throwable , zend_string * service_name , uint64_t time , ddog_SpanBytes * span ) {
361361 if (!ddtrace_exception_debugging_is_active ()) {
362362 return ;
363363 }
@@ -392,14 +392,14 @@ static void ddtrace_collect_exception_debug_data(zend_object *exception, zend_st
392392 char * exception_id = zend_arena_alloc (& DDTRACE_G (debugger_capture_arena ).arena , uuid_len );
393393 ddog_snapshot_format_new_uuid ((uint8_t (* )[uuid_len ])exception_id );
394394
395- ddog_add_str_span_meta_CharSlice (span , "_dd.debug.error.exception_capture_id " , (ddog_CharSlice ){.ptr = exception_id , .len = uuid_len });
395+ ddog_add_str_span_meta_CharSlice (span , "_dd.debug.error.exception_id " , (ddog_CharSlice ){.ptr = exception_id , .len = uuid_len });
396396
397397 memset (& DDTRACE_G (exception_debugger_buffer ), 0 , sizeof (DDTRACE_G (exception_debugger_buffer )));
398398
399399 zval * frame ;
400400 int frame_num = 0 ;
401401 ZEND_HASH_FOREACH_NUM_KEY_VAL (Z_ARR_P (trace ), frame_num , frame ) {
402- if (get_DD_EXCEPTION_REPLAY_CAPTURE_MAX_FRAMES () >= 0 && get_DD_EXCEPTION_REPLAY_CAPTURE_MAX_FRAMES () < frame_num ) {
402+ if (get_DD_EXCEPTION_REPLAY_CAPTURE_MAX_FRAMES () >= 0 && get_DD_EXCEPTION_REPLAY_CAPTURE_MAX_FRAMES () <= frame_num ) {
403403 break ;
404404 }
405405
@@ -423,6 +423,10 @@ static void ddtrace_collect_exception_debug_data(zend_object *exception, zend_st
423423 }
424424
425425 ddog_DebuggerCapture * capture = dd_create_frame_and_collect_locals (exception_id , exception_hash , frame_num , class_slice , func_slice , locals , service_name , & capture_config , time , span );
426+ zend_string * ex_msg = zai_exception_message (throwable );
427+ ddog_snapshot_set_throwable (capture ,
428+ (ddog_CharSlice ){ .ptr = ZSTR_VAL (throwable -> ce -> name ), .len = ZSTR_LEN (throwable -> ce -> name ) },
429+ dd_zend_string_to_CharSlice (ex_msg ));
426430 locals = zend_hash_find (Z_ARR_P (frame ), key_locals );
427431
428432 zend_string * key ;
@@ -431,7 +435,7 @@ static void ddtrace_collect_exception_debug_data(zend_object *exception, zend_st
431435 if (args && Z_TYPE_P (args ) == IS_ARRAY ) {
432436 zend_long idx ;
433437 ZEND_HASH_FOREACH_KEY_VAL (Z_ARR_P (args ), idx , key , val ) {
434- struct ddog_CaptureValue capture_value = {0 };
438+ ddog_CaptureValue capture_value = {0 };
435439 ddtrace_create_capture_value (val , & capture_value , & capture_config , capture_config .max_reference_depth );
436440 ddog_CharSlice arg_name ;
437441 if (key ) {
@@ -455,15 +459,19 @@ static void ddtrace_collect_exception_debug_data(zend_object *exception, zend_st
455459
456460 zval * obj = zend_hash_find (Z_ARR_P (frame ), ZSTR_KNOWN (ZEND_STR_OBJECT ));
457461 if (obj ) {
458- struct ddog_CaptureValue capture_value = {0 };
462+ ddog_CaptureValue capture_value = {0 };
459463 ddtrace_create_capture_value (obj , & capture_value , & capture_config , capture_config .max_reference_depth );
460464 ddog_snapshot_add_field (capture , DDOG_FIELD_TYPE_ARG , DDOG_CHARSLICE_C ("this" ), capture_value );
461465 }
462466 } ZEND_HASH_FOREACH_END ();
463467
464- if (get_DD_EXCEPTION_REPLAY_CAPTURE_MAX_FRAMES () < 0 || get_DD_EXCEPTION_REPLAY_CAPTURE_MAX_FRAMES () > frame_num ) {
468+ if (get_DD_EXCEPTION_REPLAY_CAPTURE_MAX_FRAMES () < 0 || get_DD_EXCEPTION_REPLAY_CAPTURE_MAX_FRAMES () > frame_num + 1 ) {
465469 if (locals && Z_TYPE_P (locals ) == IS_ARRAY ) {
466- dd_create_frame_and_collect_locals (exception_id , exception_hash , frame_num + 1 , DDOG_CHARSLICE_C ("" ), DDOG_CHARSLICE_C ("" ), locals , service_name , & capture_config , time , span );
470+ ddog_DebuggerCapture * last_capture = dd_create_frame_and_collect_locals (exception_id , exception_hash , frame_num + 1 , DDOG_CHARSLICE_C ("" ), DDOG_CHARSLICE_C ("" ), locals , service_name , & capture_config , time , span );
471+ zend_string * last_ex_msg = zai_exception_message (throwable );
472+ ddog_snapshot_set_throwable (last_capture ,
473+ (ddog_CharSlice ){ .ptr = ZSTR_VAL (throwable -> ce -> name ), .len = ZSTR_LEN (throwable -> ce -> name ) },
474+ dd_zend_string_to_CharSlice (last_ex_msg ));
467475 }
468476 }
469477
@@ -508,8 +516,9 @@ void ddtrace_exception_to_meta(zend_object *exception, zend_string *service_name
508516 previous = zai_exception_read_property (exception , ZSTR_KNOWN (ZEND_STR_PREVIOUS ));
509517 }
510518
511- // exception is now the innermost exception, i.e. what we need
512- ddtrace_collect_exception_debug_data (exception , service_name , time / 1000000 , span );
519+ // Capture frames from the innermost (root cause) exception so the stack trace reflects
520+ // where the error originated. Use the outermost exception for the throwable type/message so it matches the span error and the exception replay filter.
521+ ddtrace_collect_exception_debug_data (exception , exception_root , service_name , time / 1000000 , span );
513522
514523 previous = zai_exception_read_property (exception_root , ZSTR_KNOWN (ZEND_STR_PREVIOUS ));
515524 while (Z_TYPE_P (previous ) == IS_OBJECT && !Z_IS_RECURSIVE_P (previous ) &&
0 commit comments