tracekit 0.2.2 → 0.2.4

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.
@@ -91,9 +91,31 @@ module Tracekit
91
91
  return if breakpoint.expire_at && Time.now > breakpoint.expire_at
92
92
  return if breakpoint.max_captures > 0 && breakpoint.capture_count >= breakpoint.max_captures
93
93
 
94
- # Apply opt-in capture depth limit
95
- if @capture_depth && @capture_depth > 0
96
- variables = limit_depth(variables, 0)
94
+ # Evaluate breakpoint condition locally for sdk-evaluable expressions
95
+ if breakpoint.condition && !breakpoint.condition.empty? && breakpoint.condition_eval == "sdk-evaluable"
96
+ begin
97
+ result = Tracekit::Evaluator.evaluate_condition(breakpoint.condition, variables)
98
+ return unless result # Condition false, skip capture
99
+ rescue Tracekit::Evaluator::UnsupportedExpressionError
100
+ # Classified as sdk-evaluable but failed locally, fall through to server
101
+ warn "TraceKit: expression classified as sdk-evaluable but failed locally, falling back to server" if ENV["DEBUG"]
102
+ rescue => e
103
+ # Other evaluation error, log and fall through to server
104
+ warn "TraceKit: condition evaluation error, falling back to server: #{e.message}" if ENV["DEBUG"]
105
+ end
106
+ end
107
+
108
+ # Logpoint mode: capture only expression results, skip locals/stack/request
109
+ if breakpoint.mode == "logpoint"
110
+ snapshot = build_logpoint_snapshot(breakpoint, file_path, line_number, function_name, label, variables)
111
+ Thread.new { submit_snapshot_with_payload_limit(snapshot, breakpoint.max_payload_bytes) }
112
+ return
113
+ end
114
+
115
+ # Apply per-breakpoint capture depth limit (with SDK-level fallback)
116
+ effective_depth = breakpoint.max_depth || @capture_depth
117
+ if effective_depth && effective_depth > 0
118
+ variables = limit_depth(variables, 0, effective_depth)
97
119
  end
98
120
 
99
121
  # Scan for security issues
@@ -104,14 +126,23 @@ module Tracekit
104
126
  span_id = nil
105
127
  if defined?(OpenTelemetry::Trace)
106
128
  span = OpenTelemetry::Trace.current_span
107
- if span && span.context.valid? && (span.context.trace_flags & OpenTelemetry::Trace::TraceFlags::SAMPLED) != 0
108
- trace_id = span.context.hex_trace_id
109
- span_id = span.context.hex_span_id
129
+ if span && span.context.valid?
130
+ sampled = begin
131
+ span.context.trace_flags.sampled?
132
+ rescue NoMethodError
133
+ # Fallback for older OTel versions
134
+ (span.context.trace_flags.to_i & 0x01) != 0 rescue false
135
+ end
136
+ if sampled
137
+ trace_id = span.context.hex_trace_id
138
+ span_id = span.context.hex_span_id
139
+ end
110
140
  end
111
141
  end
112
142
 
113
- # Get stack trace
114
- stack_trace = caller.join("\n")
143
+ # Get stack trace with dynamic depth from per-breakpoint config
144
+ effective_stack_depth = breakpoint.stack_depth || 50
145
+ stack_trace = caller(1, effective_stack_depth).join("\n")
115
146
 
116
147
  snapshot = Snapshot.new(
117
148
  breakpoint_id: breakpoint.id,
@@ -128,24 +159,9 @@ module Tracekit
128
159
  captured_at: Time.now.utc.iso8601
129
160
  )
130
161
 
131
- # Apply opt-in max payload limit
132
- serialized = JSON.generate(snapshot.to_h)
133
- if @max_payload && @max_payload > 0 && serialized.bytesize > @max_payload
134
- snapshot = Snapshot.new(
135
- breakpoint_id: breakpoint.id,
136
- service_name: @service_name,
137
- file_path: file_path,
138
- function_name: function_name,
139
- label: label,
140
- line_number: line_number,
141
- variables: { "_truncated" => true, "_payload_size" => serialized.bytesize, "_max_payload" => @max_payload },
142
- security_flags: [],
143
- stack_trace: stack_trace,
144
- trace_id: trace_id,
145
- span_id: span_id,
146
- captured_at: Time.now.utc.iso8601
147
- )
148
- end
162
+ # Apply per-breakpoint max payload limit (with SDK-level fallback)
163
+ effective_max_payload = breakpoint.max_payload_bytes || @max_payload
164
+ submit_snapshot_with_payload_limit(snapshot, effective_max_payload)
149
165
 
150
166
  # Submit asynchronously (with optional timeout)
151
167
  if @capture_timeout && @capture_timeout > 0
@@ -169,24 +185,77 @@ module Tracekit
169
185
 
170
186
  private
171
187
 
172
- # Limit variable nesting depth (opt-in)
173
- def limit_depth(data, current_depth)
174
- return { "_truncated" => true, "_depth" => current_depth } if current_depth >= @capture_depth
188
+ # Limit variable nesting depth (opt-in, supports per-breakpoint override)
189
+ def limit_depth(data, current_depth, max_depth = nil)
190
+ effective_depth = max_depth || @capture_depth
191
+ return { "_truncated" => true, "_depth" => current_depth } if current_depth >= effective_depth
175
192
 
176
193
  case data
177
194
  when Hash
178
195
  result = {}
179
196
  data.each do |k, v|
180
- result[k] = limit_depth(v, current_depth + 1)
197
+ result[k] = limit_depth(v, current_depth + 1, effective_depth)
181
198
  end
182
199
  result
183
200
  when Array
184
- data.map { |item| limit_depth(item, current_depth + 1) }
201
+ data.map { |item| limit_depth(item, current_depth + 1, effective_depth) }
185
202
  else
186
203
  data
187
204
  end
188
205
  end
189
206
 
207
+ # Build a logpoint snapshot: expression results only, no locals/stack
208
+ def build_logpoint_snapshot(breakpoint, file_path, line_number, function_name, label, variables)
209
+ expression_results = {}
210
+ if breakpoint.capture_expressions && !breakpoint.capture_expressions.empty?
211
+ expression_results = Tracekit::Evaluator.evaluate_expressions(
212
+ breakpoint.capture_expressions, variables
213
+ )
214
+ end
215
+
216
+ Snapshot.new(
217
+ breakpoint_id: breakpoint.id,
218
+ service_name: @service_name,
219
+ file_path: file_path,
220
+ function_name: function_name,
221
+ label: label,
222
+ line_number: line_number,
223
+ variables: {},
224
+ security_flags: [],
225
+ stack_trace: "",
226
+ trace_id: nil,
227
+ span_id: nil,
228
+ captured_at: Time.now.utc.iso8601,
229
+ expression_results: expression_results,
230
+ mode: "logpoint"
231
+ )
232
+ end
233
+
234
+ # Submit snapshot with payload limit check
235
+ def submit_snapshot_with_payload_limit(snapshot, max_payload_bytes)
236
+ effective_limit = max_payload_bytes || @max_payload
237
+ if effective_limit && effective_limit > 0
238
+ serialized = JSON.generate(snapshot.to_h)
239
+ if serialized.bytesize > effective_limit
240
+ snapshot = Snapshot.new(
241
+ breakpoint_id: snapshot.breakpoint_id,
242
+ service_name: snapshot.service_name,
243
+ file_path: snapshot.file_path,
244
+ function_name: snapshot.function_name,
245
+ label: snapshot.label,
246
+ line_number: snapshot.line_number,
247
+ variables: { "_truncated" => true, "_payload_size" => serialized.bytesize, "_max_payload" => effective_limit },
248
+ security_flags: [],
249
+ stack_trace: snapshot.stack_trace,
250
+ trace_id: snapshot.trace_id,
251
+ span_id: snapshot.span_id,
252
+ captured_at: snapshot.captured_at
253
+ )
254
+ end
255
+ end
256
+ Thread.new { submit_snapshot(snapshot) }
257
+ end
258
+
190
259
  def fetch_active_breakpoints
191
260
  url = "#{@base_url}/sdk/snapshots/active/#{@service_name}"
192
261
  uri = URI(url)
@@ -237,17 +306,7 @@ module Tracekit
237
306
  @breakpoints_cache.clear
238
307
 
239
308
  breakpoints.each do |bp_data|
240
- bp = BreakpointConfig.new(
241
- id: bp_data[:id],
242
- file_path: bp_data[:file_path],
243
- line_number: bp_data[:line_number],
244
- function_name: bp_data[:function_name],
245
- label: bp_data[:label],
246
- enabled: bp_data[:enabled],
247
- max_captures: bp_data[:max_captures] || 0,
248
- capture_count: bp_data[:capture_count] || 0,
249
- expire_at: bp_data[:expire_at] ? Time.parse(bp_data[:expire_at]) : nil
250
- )
309
+ bp = build_breakpoint_config(bp_data)
251
310
 
252
311
  # Key by function + label
253
312
  if bp.label && bp.function_name
@@ -303,6 +362,7 @@ module Tracekit
303
362
  end
304
363
 
305
364
  def submit_snapshot(snapshot)
365
+ # Circuit breaker check
306
366
  # Circuit breaker check
307
367
  return unless circuit_breaker_should_allow?
308
368
 
@@ -455,9 +515,9 @@ module Tracekit
455
515
  warn "TraceKit: SSE event handling error: #{e.message}" if ENV["DEBUG"]
456
516
  end
457
517
 
458
- # Upsert a single breakpoint into the cache
459
- def upsert_breakpoint(bp_data)
460
- bp = BreakpointConfig.new(
518
+ # Build a BreakpointConfig from parsed payload data
519
+ def build_breakpoint_config(bp_data)
520
+ BreakpointConfig.new(
461
521
  id: bp_data[:id],
462
522
  file_path: bp_data[:file_path],
463
523
  line_number: bp_data[:line_number],
@@ -466,8 +526,21 @@ module Tracekit
466
526
  enabled: bp_data[:enabled],
467
527
  max_captures: bp_data[:max_captures] || 0,
468
528
  capture_count: bp_data[:capture_count] || 0,
469
- expire_at: bp_data[:expire_at] ? Time.parse(bp_data[:expire_at]) : nil
529
+ expire_at: bp_data[:expire_at] ? Time.parse(bp_data[:expire_at]) : nil,
530
+ condition: bp_data[:condition],
531
+ condition_eval: bp_data[:condition_eval],
532
+ mode: bp_data[:mode],
533
+ stack_depth: bp_data[:stack_depth],
534
+ max_depth: bp_data[:max_depth],
535
+ max_payload_bytes: bp_data[:max_payload_bytes],
536
+ capture_expressions: bp_data[:capture_expressions],
537
+ idle_timeout_hours: bp_data[:idle_timeout_hours]
470
538
  )
539
+ end
540
+
541
+ # Upsert a single breakpoint into the cache
542
+ def upsert_breakpoint(bp_data)
543
+ bp = build_breakpoint_config(bp_data)
471
544
 
472
545
  # Key by function + label
473
546
  if bp.label && bp.function_name
@@ -6,6 +6,10 @@ module Tracekit
6
6
  BreakpointConfig = Struct.new(
7
7
  :id, :file_path, :line_number, :function_name, :label,
8
8
  :enabled, :max_captures, :capture_count, :expire_at,
9
+ # v25 capture features
10
+ :condition, :condition_eval, :mode, :stack_depth,
11
+ :max_depth, :max_payload_bytes, :capture_expressions,
12
+ :idle_timeout_hours,
9
13
  keyword_init: true
10
14
  )
11
15
 
@@ -14,6 +18,8 @@ module Tracekit
14
18
  :breakpoint_id, :service_name, :file_path, :function_name, :label,
15
19
  :line_number, :variables, :security_flags, :stack_trace,
16
20
  :trace_id, :span_id, :captured_at,
21
+ # v25 capture features
22
+ :expression_results, :mode,
17
23
  keyword_init: true
18
24
  )
19
25
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tracekit
4
- VERSION = "0.2.2"
4
+ VERSION = "0.2.4"
5
5
  end
data/lib/tracekit.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require_relative "tracekit/version"
4
4
  require_relative "tracekit/config"
5
5
  require_relative "tracekit/endpoint_resolver"
6
+ require_relative "tracekit/evaluator"
6
7
 
7
8
  # Metrics
8
9
  require_relative "tracekit/metrics/metric_data_point"
@@ -23,6 +24,15 @@ require_relative "tracekit/local_ui_detector"
23
24
  require_relative "tracekit/snapshots/models"
24
25
  require_relative "tracekit/snapshots/client"
25
26
 
27
+ # LLM instrumentation
28
+ begin
29
+ require_relative "tracekit/llm/common"
30
+ require_relative "tracekit/llm/openai_instrumentation"
31
+ require_relative "tracekit/llm/anthropic_instrumentation"
32
+ rescue LoadError
33
+ # LLM instrumentation not available
34
+ end
35
+
26
36
  # Core SDK
27
37
  require_relative "tracekit/sdk"
28
38
  require_relative "tracekit/middleware"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tracekit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - TraceKit
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-03-08 00:00:00.000000000 Z
11
+ date: 2026-04-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: opentelemetry-sdk
@@ -150,6 +150,10 @@ files:
150
150
  - lib/tracekit.rb
151
151
  - lib/tracekit/config.rb
152
152
  - lib/tracekit/endpoint_resolver.rb
153
+ - lib/tracekit/evaluator.rb
154
+ - lib/tracekit/llm/anthropic_instrumentation.rb
155
+ - lib/tracekit/llm/common.rb
156
+ - lib/tracekit/llm/openai_instrumentation.rb
153
157
  - lib/tracekit/local_ui/detector.rb
154
158
  - lib/tracekit/local_ui_detector.rb
155
159
  - lib/tracekit/metrics/counter.rb
@@ -173,7 +177,7 @@ metadata:
173
177
  homepage_uri: https://github.com/Tracekit-Dev/ruby-sdk
174
178
  source_code_uri: https://github.com/Tracekit-Dev/ruby-sdk
175
179
  changelog_uri: https://github.com/Tracekit-Dev/ruby-sdk/blob/main/CHANGELOG.md
176
- post_install_message:
180
+ post_install_message:
177
181
  rdoc_options: []
178
182
  require_paths:
179
183
  - lib
@@ -188,8 +192,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
188
192
  - !ruby/object:Gem::Version
189
193
  version: '0'
190
194
  requirements: []
191
- rubygems_version: 3.5.3
192
- signing_key:
195
+ rubygems_version: 3.0.3.1
196
+ signing_key:
193
197
  specification_version: 4
194
198
  summary: TraceKit Ruby SDK - OpenTelemetry-based APM for Ruby applications
195
199
  test_files: []