datadog 2.28.0 → 2.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +46 -2
  3. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +64 -3
  4. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +23 -4
  5. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +3 -1
  6. data/ext/datadog_profiling_native_extension/extconf.rb +5 -0
  7. data/ext/datadog_profiling_native_extension/heap_recorder.c +183 -51
  8. data/ext/datadog_profiling_native_extension/heap_recorder.h +12 -1
  9. data/ext/datadog_profiling_native_extension/stack_recorder.c +34 -5
  10. data/ext/datadog_profiling_native_extension/stack_recorder.h +2 -1
  11. data/ext/libdatadog_api/crashtracker.c +5 -0
  12. data/ext/libdatadog_api/crashtracker_report_exception.c +236 -0
  13. data/lib/datadog/appsec/assets/blocked.html +2 -1
  14. data/lib/datadog/appsec/configuration/settings.rb +14 -0
  15. data/lib/datadog/appsec/context.rb +44 -9
  16. data/lib/datadog/appsec/contrib/excon/ssrf_detection_middleware.rb +54 -5
  17. data/lib/datadog/appsec/contrib/faraday/integration.rb +1 -1
  18. data/lib/datadog/appsec/contrib/faraday/patcher.rb +1 -1
  19. data/lib/datadog/appsec/contrib/faraday/ssrf_detection_middleware.rb +60 -7
  20. data/lib/datadog/appsec/contrib/graphql/gateway/multiplex.rb +11 -6
  21. data/lib/datadog/appsec/contrib/graphql/integration.rb +1 -1
  22. data/lib/datadog/appsec/contrib/rack/gateway/request.rb +6 -10
  23. data/lib/datadog/appsec/contrib/rack/request_middleware.rb +1 -3
  24. data/lib/datadog/appsec/contrib/rails/patches/process_action_patch.rb +2 -0
  25. data/lib/datadog/appsec/contrib/rest_client/request_ssrf_detection_patch.rb +72 -7
  26. data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +2 -0
  27. data/lib/datadog/appsec/counter_sampler.rb +25 -0
  28. data/lib/datadog/appsec/metrics/telemetry_exporter.rb +18 -0
  29. data/lib/datadog/appsec/security_engine/engine.rb +23 -2
  30. data/lib/datadog/appsec/utils/http/body.rb +38 -0
  31. data/lib/datadog/appsec/utils/http/media_range.rb +2 -1
  32. data/lib/datadog/appsec/utils/http/media_type.rb +28 -35
  33. data/lib/datadog/appsec/utils/http/url_encoded.rb +52 -0
  34. data/lib/datadog/core/configuration/components.rb +29 -4
  35. data/lib/datadog/core/configuration/supported_configurations.rb +3 -0
  36. data/lib/datadog/core/configuration.rb +2 -2
  37. data/lib/datadog/core/crashtracking/component.rb +79 -19
  38. data/lib/datadog/core/environment/agent_info.rb +65 -1
  39. data/lib/datadog/core/logger.rb +1 -1
  40. data/lib/datadog/core/metrics/logging.rb +1 -1
  41. data/lib/datadog/core/process_discovery.rb +15 -19
  42. data/lib/datadog/core/rate_limiter.rb +2 -0
  43. data/lib/datadog/core/remote/component.rb +16 -5
  44. data/lib/datadog/core/remote/transport/config.rb +5 -11
  45. data/lib/datadog/core/telemetry/component.rb +0 -13
  46. data/lib/datadog/core/telemetry/transport/telemetry.rb +5 -6
  47. data/lib/datadog/core/transport/ext.rb +1 -0
  48. data/lib/datadog/core/transport/http/response.rb +4 -0
  49. data/lib/datadog/core/transport/parcel.rb +61 -9
  50. data/lib/datadog/core/utils/fnv.rb +26 -0
  51. data/lib/datadog/core.rb +6 -1
  52. data/lib/datadog/data_streams/processor.rb +34 -33
  53. data/lib/datadog/data_streams/transport/http/stats.rb +6 -0
  54. data/lib/datadog/data_streams/transport/http.rb +0 -4
  55. data/lib/datadog/data_streams/transport/stats.rb +5 -12
  56. data/lib/datadog/di/component.rb +1 -1
  57. data/lib/datadog/di/configuration/settings.rb +9 -0
  58. data/lib/datadog/di/context.rb +6 -0
  59. data/lib/datadog/di/instrumenter.rb +178 -133
  60. data/lib/datadog/di/probe.rb +10 -1
  61. data/lib/datadog/di/probe_file_loader.rb +2 -2
  62. data/lib/datadog/di/probe_manager.rb +7 -2
  63. data/lib/datadog/di/probe_notification_builder.rb +29 -8
  64. data/lib/datadog/di/probe_notifier_worker.rb +13 -3
  65. data/lib/datadog/di/proc_responder.rb +4 -0
  66. data/lib/datadog/di/remote.rb +2 -2
  67. data/lib/datadog/di/transport/diagnostics.rb +5 -7
  68. data/lib/datadog/di/transport/http/diagnostics.rb +3 -1
  69. data/lib/datadog/di/transport/http/input.rb +1 -1
  70. data/lib/datadog/di/transport/input.rb +5 -6
  71. data/lib/datadog/kit/tracing/method_tracer.rb +132 -0
  72. data/lib/datadog/open_feature/transport.rb +8 -11
  73. data/lib/datadog/profiling/component.rb +0 -6
  74. data/lib/datadog/tracing/contrib/http/integration.rb +0 -2
  75. data/lib/datadog/tracing/contrib/mysql2/configuration/settings.rb +6 -0
  76. data/lib/datadog/tracing/contrib/mysql2/instrumentation.rb +2 -1
  77. data/lib/datadog/tracing/contrib/pg/configuration/settings.rb +6 -0
  78. data/lib/datadog/tracing/contrib/pg/instrumentation.rb +2 -1
  79. data/lib/datadog/tracing/contrib/propagation/sql_comment/ext.rb +10 -0
  80. data/lib/datadog/tracing/contrib/propagation/sql_comment/mode.rb +5 -1
  81. data/lib/datadog/tracing/contrib/propagation/sql_comment.rb +24 -0
  82. data/lib/datadog/tracing/contrib/rack/route_inference.rb +18 -6
  83. data/lib/datadog/tracing/contrib/registerable.rb +11 -0
  84. data/lib/datadog/tracing/contrib/sneakers/integration.rb +15 -4
  85. data/lib/datadog/tracing/contrib/trilogy/configuration/settings.rb +6 -0
  86. data/lib/datadog/tracing/contrib/trilogy/instrumentation.rb +3 -1
  87. data/lib/datadog/tracing/transport/io/client.rb +5 -8
  88. data/lib/datadog/tracing/transport/io/traces.rb +28 -34
  89. data/lib/datadog/tracing/transport/traces.rb +4 -10
  90. data/lib/datadog/version.rb +1 -1
  91. metadata +10 -5
  92. data/lib/datadog/appsec/contrib/rails/ext.rb +0 -13
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 91ec2ddb3720f61843ee7526cd4e3493a646ab7afaf3d6e4cce71019d1dfd582
4
- data.tar.gz: d0996f8b1eb568aea751a1e781fa4d9dcc7e1d436911fd579c7aa43947340f32
3
+ metadata.gz: 6f762ad624ddb0a21d753a524132de0f59e617e63f723fabc6c305949a77554d
4
+ data.tar.gz: 217159cbaea1102ba47a97deb73927919d8152264701322719aad722391d75aa
5
5
  SHA512:
6
- metadata.gz: fb8da926cd9a09a500c781f60ad5fb35c72c9b3162acc067229f88d01281c63c6753c4230a2cf56f803fb0174994707b159e31f5d584e6682313a13e2187b61e
7
- data.tar.gz: 8c93304f737b5798af5d6ed520f7006d7aa2e7644243fbfcd38994d66dde0aefb53084bca96024e61a7cbebcb72e60f958715fe4864c1e7fe90733103f9cb527
6
+ metadata.gz: 711466d29940c6b22b5964744e7d1b6dbf5a002ca5992dbc01d61718f16cc66b33dcc7bee2604c4c2ba64a9efb8014114d015d44a109e1b5f55b1375d36c927a
7
+ data.tar.gz: 1490e2ab41fdf0c7ce673bbb7eaa1ccc28073cd757576a30734ce3674bef89fd58aa2876b2e9d2e26e12119e45b1d90f787e260d86cfc6e1fa5055bf14ba16b0
data/CHANGELOG.md CHANGED
@@ -2,6 +2,34 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [2.29.0] - 2026-02-20
6
+
7
+
8
+ ### Added
9
+
10
+ * AppSec: Add analysis for downstream request with redirects ([#5347][])
11
+ * AppSec: Add downstream request body analysis. ([#5320][])
12
+ * SSI: Add support for Bundler vendored mode (`BUNDLE_PATH`) ([#5368][])
13
+ * SSI: Default to local dependency resolution ([#5368][])
14
+ * Dynamic Instrumentation: Added circuit breaker to automatically disable probes consuming excessive CPU time ([#5335][])
15
+ * Crashtracking: Add reporting of unhandled exceptions ([#5321][])
16
+ * Tracing: Add support for the `kicks` gem ([#5305][])
17
+ * Profiling: Add heap profiling for ruby 4.x ([#5201][])
18
+ * Tracing: Add simple method tracing API ([#5294][])
19
+ * Tracing: Add `trace_singleton_class_method` for tracing singleton class methods. ([#5334][])
20
+
21
+ ### Changed
22
+
23
+ * Dynamic Instrumentation: Improve error reporting when instrumentation fails or is removed due to circuit breaker ([#5371][])
24
+ * SSI: Reduce SSI package size ([#5352][])
25
+ * Core: Change default logger output from stdout to stderr ([#5342][])
26
+ * AppSec: Make AppSec blocking page more friendly for vulnerability scanners ([#5341][])
27
+ * Core: Add process tags and container id to process discovery payloads when the experimental setting `DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED=true` is enabled. ([#5336][])
28
+
29
+ ### Fixed
30
+
31
+ * Dynamic Instrumentation: Fix Live Debugger UI for forking web servers with more than one worker process ([#5304][])
32
+
5
33
  ## [2.28.0] - 2026-02-04
6
34
 
7
35
  ### Added
@@ -3486,7 +3514,8 @@ Release notes: https://github.com/DataDog/dd-trace-rb/releases/tag/v0.3.1
3486
3514
  Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
3487
3515
 
3488
3516
 
3489
- [Unreleased]: https://github.com/DataDog/dd-trace-rb/compare/v2.28.0...master
3517
+ [Unreleased]: https://github.com/DataDog/dd-trace-rb/compare/v2.29.0...master
3518
+ [2.29.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.28.0...v2.29.0
3490
3519
  [2.28.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.27.0...v2.28.0
3491
3520
  [2.27.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.26.0...v2.27.0
3492
3521
  [2.26.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.25.0...v2.26.0
@@ -5153,6 +5182,7 @@ Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
5153
5182
  [#5176]: https://github.com/DataDog/dd-trace-rb/issues/5176
5154
5183
  [#5194]: https://github.com/DataDog/dd-trace-rb/issues/5194
5155
5184
  [#5197]: https://github.com/DataDog/dd-trace-rb/issues/5197
5185
+ [#5201]: https://github.com/DataDog/dd-trace-rb/issues/5201
5156
5186
  [#5206]: https://github.com/DataDog/dd-trace-rb/issues/5206
5157
5187
  [#5210]: https://github.com/DataDog/dd-trace-rb/issues/5210
5158
5188
  [#5215]: https://github.com/DataDog/dd-trace-rb/issues/5215
@@ -5166,6 +5196,20 @@ Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
5166
5196
  [#5273]: https://github.com/DataDog/dd-trace-rb/issues/5273
5167
5197
  [#5278]: https://github.com/DataDog/dd-trace-rb/issues/5278
5168
5198
  [#5283]: https://github.com/DataDog/dd-trace-rb/issues/5283
5199
+ [#5294]: https://github.com/DataDog/dd-trace-rb/issues/5294
5200
+ [#5304]: https://github.com/DataDog/dd-trace-rb/issues/5304
5201
+ [#5305]: https://github.com/DataDog/dd-trace-rb/issues/5305
5202
+ [#5320]: https://github.com/DataDog/dd-trace-rb/issues/5320
5203
+ [#5321]: https://github.com/DataDog/dd-trace-rb/issues/5321
5204
+ [#5334]: https://github.com/DataDog/dd-trace-rb/issues/5334
5205
+ [#5335]: https://github.com/DataDog/dd-trace-rb/issues/5335
5206
+ [#5336]: https://github.com/DataDog/dd-trace-rb/issues/5336
5207
+ [#5341]: https://github.com/DataDog/dd-trace-rb/issues/5341
5208
+ [#5342]: https://github.com/DataDog/dd-trace-rb/issues/5342
5209
+ [#5347]: https://github.com/DataDog/dd-trace-rb/issues/5347
5210
+ [#5352]: https://github.com/DataDog/dd-trace-rb/issues/5352
5211
+ [#5368]: https://github.com/DataDog/dd-trace-rb/issues/5368
5212
+ [#5371]: https://github.com/DataDog/dd-trace-rb/issues/5371
5169
5213
  [@AdrianLC]: https://github.com/AdrianLC
5170
5214
  [@Azure7111]: https://github.com/Azure7111
5171
5215
  [@BabyGroot]: https://github.com/BabyGroot
@@ -5320,4 +5364,4 @@ Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
5320
5364
  [@y-yagi]: https://github.com/y-yagi
5321
5365
  [@yujideveloper]: https://github.com/yujideveloper
5322
5366
  [@yukimurasawa]: https://github.com/yukimurasawa
5323
- [@zachmccormick]: https://github.com/zachmccormick
5367
+ [@zachmccormick]: https://github.com/zachmccormick
@@ -87,6 +87,7 @@ unsigned int MAX_ALLOC_WEIGHT = 10000;
87
87
  static rb_postponed_job_handle_t sample_from_postponed_job_handle;
88
88
  static rb_postponed_job_handle_t after_gc_from_postponed_job_handle;
89
89
  static rb_postponed_job_handle_t after_gvl_running_from_postponed_job_handle;
90
+ static rb_postponed_job_handle_t after_allocation_from_postponed_job_handle;
90
91
  #endif
91
92
 
92
93
  // Contains state for a single CpuAndWallTimeWorker instance
@@ -138,6 +139,8 @@ typedef struct {
138
139
  // # Generic stats
139
140
  // How many times we tried to trigger a sample
140
141
  unsigned int trigger_sample_attempts;
142
+ // How many times extra sleep was triggered by dynamic sampling rate
143
+ unsigned int trigger_sample_extra_sleep;
141
144
  // How many times we tried to simulate signal delivery
142
145
  unsigned int trigger_simulated_signal_delivery_attempts;
143
146
  // How many times we actually simulated signal delivery
@@ -229,6 +232,7 @@ static void on_newobj_event(DDTRACE_UNUSED VALUE unused1, DDTRACE_UNUSED void *u
229
232
  static void disable_tracepoints(cpu_and_wall_time_worker_state *state);
230
233
  static VALUE _native_with_blocked_sigprof(DDTRACE_UNUSED VALUE self);
231
234
  static VALUE rescued_sample_allocation(VALUE tracepoint_data);
235
+ static VALUE rescued_after_allocation(VALUE self_instance);
232
236
  static void delayed_error(cpu_and_wall_time_worker_state *state, const char *error);
233
237
  static void delayed_error_clock_failure(cpu_and_wall_time_worker_state *state);
234
238
  static VALUE _native_delayed_error(DDTRACE_UNUSED VALUE self, VALUE instance, VALUE error_msg);
@@ -243,9 +247,11 @@ static VALUE _native_gvl_profiling_hook_active(DDTRACE_UNUSED VALUE self, VALUE
243
247
  static VALUE handle_sampling_failure_rescued_sample_from_postponed_job(VALUE self_instance, VALUE exception);
244
248
  static VALUE handle_sampling_failure_thread_context_collector_sample_after_gc(VALUE self_instance, VALUE exception);
245
249
  static VALUE handle_sampling_failure_rescued_sample_allocation(VALUE self_instance, VALUE exception);
250
+ static VALUE handle_sampling_failure_rescued_after_allocation(VALUE self_instance, VALUE exception);
246
251
  static VALUE handle_sampling_failure_rescued_after_gvl_running_from_postponed_job(VALUE self_instance, VALUE exception);
247
252
  static inline void during_sample_enter(cpu_and_wall_time_worker_state* state);
248
253
  static inline void during_sample_exit(cpu_and_wall_time_worker_state* state);
254
+ static void after_allocation_from_postponed_job(DDTRACE_UNUSED void *_unused);
249
255
 
250
256
  // We're using `on_newobj_event` function with `rb_add_event_hook2`, which requires in its public signature a function
251
257
  // with signature `rb_event_hook_func_t` which doesn't match `on_newobj_event`.
@@ -293,11 +299,13 @@ void collectors_cpu_and_wall_time_worker_init(VALUE profiling_module) {
293
299
  sample_from_postponed_job_handle = rb_postponed_job_preregister(unused_flags, sample_from_postponed_job, NULL);
294
300
  after_gc_from_postponed_job_handle = rb_postponed_job_preregister(unused_flags, after_gc_from_postponed_job, NULL);
295
301
  after_gvl_running_from_postponed_job_handle = rb_postponed_job_preregister(unused_flags, after_gvl_running_from_postponed_job, NULL);
302
+ after_allocation_from_postponed_job_handle = rb_postponed_job_preregister(unused_flags, after_allocation_from_postponed_job, NULL);
296
303
 
297
304
  if (
298
305
  sample_from_postponed_job_handle == POSTPONED_JOB_HANDLE_INVALID ||
299
306
  after_gc_from_postponed_job_handle == POSTPONED_JOB_HANDLE_INVALID ||
300
- after_gvl_running_from_postponed_job_handle == POSTPONED_JOB_HANDLE_INVALID
307
+ after_gvl_running_from_postponed_job_handle == POSTPONED_JOB_HANDLE_INVALID ||
308
+ after_allocation_from_postponed_job_handle == POSTPONED_JOB_HANDLE_INVALID
301
309
  ) {
302
310
  raise_error(rb_eRuntimeError, "Failed to register profiler postponed jobs (got POSTPONED_JOB_HANDLE_INVALID)");
303
311
  }
@@ -713,7 +721,10 @@ static void *run_sampling_trigger_loop(void *state_ptr) {
713
721
  // `dynamic_sampling_rate_get_sleep` may have changed while the above sleep was ongoing.
714
722
  uint64_t extra_sleep =
715
723
  dynamic_sampling_rate_get_sleep(&state->cpu_dynamic_sampling_rate, monotonic_wall_time_now_ns(DO_NOT_RAISE_ON_FAILURE));
716
- if (state->dynamic_sampling_rate_enabled && extra_sleep > 0) sleep_for(extra_sleep);
724
+ if (state->dynamic_sampling_rate_enabled && extra_sleep > 0) {
725
+ state->stats.trigger_sample_extra_sleep++;
726
+ sleep_for(extra_sleep);
727
+ }
717
728
  }
718
729
 
719
730
  return NULL; // Unused
@@ -1049,6 +1060,7 @@ static VALUE _native_stats(DDTRACE_UNUSED VALUE self, VALUE instance) {
1049
1060
  VALUE stats_as_hash = rb_hash_new();
1050
1061
  VALUE arguments[] = {
1051
1062
  ID2SYM(rb_intern("trigger_sample_attempts")), /* => */ UINT2NUM(state->stats.trigger_sample_attempts),
1063
+ ID2SYM(rb_intern("trigger_sample_extra_sleep")), /* => */ UINT2NUM(state->stats.trigger_sample_extra_sleep),
1052
1064
  ID2SYM(rb_intern("trigger_simulated_signal_delivery_attempts")), /* => */ UINT2NUM(state->stats.trigger_simulated_signal_delivery_attempts),
1053
1065
  ID2SYM(rb_intern("simulated_signal_delivery")), /* => */ UINT2NUM(state->stats.simulated_signal_delivery),
1054
1066
  ID2SYM(rb_intern("signal_handler_enqueued_sample")), /* => */ UINT2NUM(state->stats.signal_handler_enqueued_sample),
@@ -1312,13 +1324,21 @@ static VALUE rescued_sample_allocation(DDTRACE_UNUSED VALUE unused) {
1312
1324
  // To control bias from sampling, we clamp the maximum weight attributed to a single allocation sample. This avoids
1313
1325
  // assigning a very large number to a sample, if for instance the dynamic sampling mechanism chose a really big interval.
1314
1326
  unsigned int weight = allocations_since_last_sample > MAX_ALLOC_WEIGHT ? MAX_ALLOC_WEIGHT : (unsigned int) allocations_since_last_sample;
1315
- thread_context_collector_sample_allocation(state->thread_context_collector_instance, weight, new_object);
1327
+ bool needs_after_allocation = thread_context_collector_sample_allocation(state->thread_context_collector_instance, weight, new_object);
1316
1328
  // ...but we still represent the skipped samples in the profile, thus the data will account for all allocations.
1317
1329
  if (weight < allocations_since_last_sample) {
1318
1330
  uint32_t skipped_samples = (uint32_t) uint64_min_of(allocations_since_last_sample - weight, UINT32_MAX);
1319
1331
  thread_context_collector_sample_skipped_allocation_samples(state->thread_context_collector_instance, skipped_samples);
1320
1332
  }
1321
1333
 
1334
+ if (needs_after_allocation) {
1335
+ #ifndef NO_POSTPONED_TRIGGER
1336
+ rb_postponed_job_trigger(after_allocation_from_postponed_job_handle);
1337
+ #else
1338
+ // Not needed on legacy rubies
1339
+ #endif
1340
+ }
1341
+
1322
1342
  // Return a dummy VALUE because we're called from rb_rescue2 which requires it
1323
1343
  return Qnil;
1324
1344
  }
@@ -1470,11 +1490,52 @@ static VALUE handle_sampling_failure_rescued_sample_allocation(VALUE self_instan
1470
1490
  return Qnil;
1471
1491
  }
1472
1492
 
1493
+ static VALUE handle_sampling_failure_rescued_after_allocation(VALUE self_instance, VALUE exception) {
1494
+ stop(self_instance, exception, "rescued_after_allocation");
1495
+ return Qnil;
1496
+ }
1497
+
1473
1498
  static VALUE handle_sampling_failure_rescued_after_gvl_running_from_postponed_job(VALUE self_instance, VALUE exception) {
1474
1499
  stop(self_instance, exception, "rescued_after_gvl_running_from_postponed_job");
1475
1500
  return Qnil;
1476
1501
  }
1477
1502
 
1503
+ static VALUE rescued_after_allocation(VALUE self_instance) {
1504
+ cpu_and_wall_time_worker_state *state;
1505
+ TypedData_Get_Struct(self_instance, cpu_and_wall_time_worker_state, &cpu_and_wall_time_worker_typed_data, state);
1506
+
1507
+ thread_context_collector_after_allocation(state->thread_context_collector_instance);
1508
+
1509
+ // Return a dummy VALUE because we're called from rb_rescue2 which requires it
1510
+ return Qnil;
1511
+ }
1512
+
1513
+ // This postponed job callback is used to finalize heap allocation recordings on Ruby 4+.
1514
+ // During on_newobj_event, calling rb_obj_id() is unsafe because it mutates the object.
1515
+ // So we defer getting the object_id until after the event completes.
1516
+ static void after_allocation_from_postponed_job(DDTRACE_UNUSED void *_unused) {
1517
+ cpu_and_wall_time_worker_state *state = active_sampler_instance_state;
1518
+
1519
+ if (state == NULL || !ddtrace_rb_ractor_main_p()) return;
1520
+
1521
+ // Protect against nested operations
1522
+ if (state->during_sample) return;
1523
+
1524
+ during_sample_enter(state);
1525
+
1526
+ // NOTE: We're not updating the allocation_sampler here.
1527
+ // This means work done in this function isn't accounted for as profiler overhead.
1528
+ // This is acceptable as the amount of work done here is expected to be small.
1529
+ safely_call(
1530
+ rescued_after_allocation,
1531
+ state->self_instance,
1532
+ state->self_instance,
1533
+ handle_sampling_failure_rescued_after_allocation
1534
+ );
1535
+
1536
+ during_sample_exit(state);
1537
+ }
1538
+
1478
1539
  static inline void during_sample_enter(cpu_and_wall_time_worker_state* state) {
1479
1540
  // Tell the compiler it's not allowed to reorder the `during_sample` flag with anything that happens after.
1480
1541
  //
@@ -1339,6 +1339,16 @@ VALUE enforce_thread_context_collector_instance(VALUE object) {
1339
1339
  return object;
1340
1340
  }
1341
1341
 
1342
+ // Finalize any pending heap allocation recordings.
1343
+ // On Ruby 4+, heap allocations are recorded in two phases: during on_newobj_event we capture
1344
+ // the object reference, then later we safely call rb_obj_id() to get the object ID.
1345
+ void thread_context_collector_after_allocation(VALUE self_instance) {
1346
+ thread_context_collector_state *state;
1347
+ TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
1348
+
1349
+ recorder_after_sample(state->recorder_instance);
1350
+ }
1351
+
1342
1352
  // This method exists only to enable testing Datadog::Profiling::Collectors::ThreadContext behavior using RSpec.
1343
1353
  // It SHOULD NOT be used for other purposes.
1344
1354
  static VALUE _native_stats(DDTRACE_UNUSED VALUE _self, VALUE collector_instance) {
@@ -1483,7 +1493,12 @@ bool thread_context_collector_prepare_sample_inside_signal_handler(VALUE self_in
1483
1493
  return prepare_sample_thread(current_thread, &thread_context->sampling_buffer);
1484
1494
  }
1485
1495
 
1486
- void thread_context_collector_sample_allocation(VALUE self_instance, unsigned int sample_weight, VALUE new_object) {
1496
+ // This method gets called from inside the RUBY_INTERNAL_EVENT_NEWOBJ tracepoint so it should never allocate in the
1497
+ // Ruby heap.
1498
+ //
1499
+ // Returns true if the after_allocation needs to be called (to do work that can't be done from inside the
1500
+ // tracepoint, such as allocate new objects), and false if it doesn't
1501
+ bool thread_context_collector_sample_allocation(VALUE self_instance, unsigned int sample_weight, VALUE new_object) {
1487
1502
  thread_context_collector_state *state;
1488
1503
  TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
1489
1504
 
@@ -1553,7 +1568,7 @@ void thread_context_collector_sample_allocation(VALUE self_instance, unsigned in
1553
1568
  class_name = ruby_vm_type; // For other weird internal things we just use the VM type
1554
1569
  }
1555
1570
 
1556
- track_object(state->recorder_instance, new_object, sample_weight, class_name);
1571
+ bool needs_after_allocation = track_object(state->recorder_instance, new_object, sample_weight, class_name);
1557
1572
 
1558
1573
  per_thread_context *thread_context = get_or_create_context_for(current_thread, state);
1559
1574
 
@@ -1570,6 +1585,8 @@ void thread_context_collector_sample_allocation(VALUE self_instance, unsigned in
1570
1585
  /* is_gvl_waiting_state: */ false,
1571
1586
  /* is_safe_to_allocate_objects: */ false // Not safe to allocate further inside the NEWOBJ tracepoint
1572
1587
  );
1588
+
1589
+ return needs_after_allocation;
1573
1590
  }
1574
1591
 
1575
1592
  // This method exists only to enable testing Datadog::Profiling::Collectors::ThreadContext behavior using RSpec.
@@ -1577,11 +1594,13 @@ void thread_context_collector_sample_allocation(VALUE self_instance, unsigned in
1577
1594
  static VALUE _native_sample_allocation(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE sample_weight, VALUE new_object) {
1578
1595
  debug_enter_unsafe_context();
1579
1596
 
1580
- thread_context_collector_sample_allocation(collector_instance, NUM2UINT(sample_weight), new_object);
1597
+ bool needs_after_allocation = thread_context_collector_sample_allocation(collector_instance, NUM2UINT(sample_weight), new_object);
1581
1598
 
1582
1599
  debug_leave_unsafe_context();
1583
1600
 
1584
- return Qtrue;
1601
+ // We could instead choose to automatically trigger the after allocation here; yet, it seems kinda nice to keep it manual for
1602
+ // the tests so we can pull on each lever separately and observe "the sausage being made" in steps
1603
+ return needs_after_allocation ? Qtrue : Qfalse;
1585
1604
  }
1586
1605
 
1587
1606
  static VALUE new_empty_thread_inner(DDTRACE_UNUSED void *arg) { return Qnil; }
@@ -11,13 +11,15 @@ void thread_context_collector_sample(
11
11
  VALUE profiler_overhead_stack_thread
12
12
  );
13
13
  __attribute__((warn_unused_result)) bool thread_context_collector_prepare_sample_inside_signal_handler(VALUE self_instance);
14
- void thread_context_collector_sample_allocation(VALUE self_instance, unsigned int sample_weight, VALUE new_object);
14
+ __attribute__((warn_unused_result)) bool thread_context_collector_sample_allocation(VALUE self_instance, unsigned int sample_weight, VALUE new_object);
15
+ void thread_context_collector_after_allocation(VALUE self_instance);
15
16
  void thread_context_collector_sample_skipped_allocation_samples(VALUE self_instance, unsigned int skipped_samples);
16
17
  VALUE thread_context_collector_sample_after_gc(VALUE self_instance);
17
18
  void thread_context_collector_on_gc_start(VALUE self_instance);
18
19
  __attribute__((warn_unused_result)) bool thread_context_collector_on_gc_finish(VALUE self_instance);
19
20
  VALUE enforce_thread_context_collector_instance(VALUE object);
20
21
 
22
+
21
23
  #ifndef NO_GVL_INSTRUMENTATION
22
24
  typedef enum {
23
25
  ON_GVL_RUNNING_UNKNOWN, // Thread is not known, it may not even be from the current Ractor
@@ -144,6 +144,11 @@ $defs << "-DNO_PRIMITIVE_MUTEX_AND_CONDITION_VARIABLE" if RUBY_VERSION < "4"
144
144
  # On Ruby 4, we can't ask the object_id from IMEMOs (https://github.com/ruby/ruby/pull/13347)
145
145
  $defs << "-DNO_IMEMO_OBJECT_ID" unless RUBY_VERSION < "4"
146
146
 
147
+ # On Ruby 4, we need to defer calling rb_obj_id during heap allocation recording
148
+ # because it's not safe to mutate objects during the newobj tracepoint
149
+ # (see https://bugs.ruby-lang.org/issues/21710)
150
+ $defs << "-DUSE_DEFERRED_HEAP_ALLOCATION_RECORDING" unless RUBY_VERSION < "4"
151
+
147
152
  # This symbol is exclusively visible on certain Ruby versions: 2.6 to 3.2, as well as 3.4 (but not 4.0+)
148
153
  # It's only used to get extra information about an object when a failure happens, so it's a "very nice to have" but not
149
154
  # actually required for correct behavior of the profiler.