promptlayer 1.1.0 → 1.2.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 (64) hide show
  1. package/README.md +22 -0
  2. package/dist/claude-agents.d.mts +20 -0
  3. package/dist/claude-agents.d.ts +20 -0
  4. package/dist/claude-agents.js +2 -0
  5. package/dist/claude-agents.js.map +1 -0
  6. package/dist/esm/{chunk-SWBNW72U.js → chunk-DFBRFJOL.js} +2 -2
  7. package/dist/esm/{chunk-SWBNW72U.js.map → chunk-DFBRFJOL.js.map} +1 -1
  8. package/dist/esm/claude-agents.js +2 -0
  9. package/dist/esm/claude-agents.js.map +1 -0
  10. package/dist/esm/index.js +1 -1
  11. package/dist/esm/openai-agents.js +2 -2
  12. package/dist/esm/openai-agents.js.map +1 -1
  13. package/dist/index.js +2 -2
  14. package/dist/index.js.map +1 -1
  15. package/dist/openai-agents.js +2 -2
  16. package/dist/openai-agents.js.map +1 -1
  17. package/package.json +18 -1
  18. package/vendor/claude-agents/trace/.claude-plugin/plugin.json +8 -0
  19. package/vendor/claude-agents/trace/hooks/hook_utils.py +38 -0
  20. package/vendor/claude-agents/trace/hooks/hooks.json +60 -0
  21. package/vendor/claude-agents/trace/hooks/lib.sh +577 -0
  22. package/vendor/claude-agents/trace/hooks/parse_stop_transcript.py +375 -0
  23. package/vendor/claude-agents/trace/hooks/post_tool_use.sh +41 -0
  24. package/vendor/claude-agents/trace/hooks/session_end.sh +37 -0
  25. package/vendor/claude-agents/trace/hooks/session_start.sh +57 -0
  26. package/vendor/claude-agents/trace/hooks/stop_hook.sh +123 -0
  27. package/vendor/claude-agents/trace/hooks/user_prompt_submit.sh +25 -0
  28. package/vendor/claude-agents/vendor_metadata.json +5 -0
  29. package/.github/CODEOWNERS +0 -1
  30. package/.github/workflows/node.js.yml +0 -30
  31. package/.github/workflows/npm-publish.yml +0 -35
  32. package/src/groups.ts +0 -16
  33. package/src/index.ts +0 -383
  34. package/src/integrations/openai-agents/helpers.test.ts +0 -254
  35. package/src/integrations/openai-agents/ids.ts +0 -27
  36. package/src/integrations/openai-agents/index.ts +0 -8
  37. package/src/integrations/openai-agents/instrumentation.test.ts +0 -46
  38. package/src/integrations/openai-agents/instrumentation.ts +0 -47
  39. package/src/integrations/openai-agents/mapping.ts +0 -714
  40. package/src/integrations/openai-agents/otlp-json.ts +0 -120
  41. package/src/integrations/openai-agents/processor.test.ts +0 -509
  42. package/src/integrations/openai-agents/processor.ts +0 -388
  43. package/src/integrations/openai-agents/time.ts +0 -56
  44. package/src/integrations/openai-agents/types.ts +0 -49
  45. package/src/integrations/openai-agents/url.ts +0 -9
  46. package/src/openai-agents.ts +0 -1
  47. package/src/promptlayer.ts +0 -125
  48. package/src/run-error-tracking.test.ts +0 -146
  49. package/src/span-exporter.ts +0 -120
  50. package/src/span-wrapper.ts +0 -51
  51. package/src/templates.ts +0 -37
  52. package/src/tracing.ts +0 -20
  53. package/src/track.ts +0 -84
  54. package/src/types.ts +0 -689
  55. package/src/utils/blueprint-builder.test.ts +0 -727
  56. package/src/utils/blueprint-builder.ts +0 -1453
  57. package/src/utils/errors.test.ts +0 -68
  58. package/src/utils/errors.ts +0 -62
  59. package/src/utils/streaming.test.ts +0 -498
  60. package/src/utils/streaming.ts +0 -1402
  61. package/src/utils/utils.ts +0 -1228
  62. package/tsconfig.json +0 -115
  63. package/tsup.config.ts +0 -20
  64. package/vitest.config.ts +0 -9
@@ -0,0 +1,577 @@
1
+ #!/bin/bash
2
+ # Shared runtime helpers for trace plugin hooks.
3
+
4
+ set -euo pipefail
5
+ umask 077
6
+
7
+ export PL_LOG_FILE="$HOME/.claude/state/promptlayer_hook.log"
8
+ export PL_QUEUE_FILE="$HOME/.claude/state/promptlayer_otlp_queue.ndjson"
9
+ export PL_SESSION_STATE_DIR="$HOME/.claude/state/promptlayer_sessions"
10
+ export PL_LOCK_DIR="$HOME/.claude/state/promptlayer_locks"
11
+
12
+ export PL_DEBUG="${PROMPTLAYER_CC_DEBUG:-false}"
13
+ export PL_API_KEY="${PROMPTLAYER_API_KEY:-}"
14
+ export PL_OTLP_ENDPOINT="${PROMPTLAYER_OTLP_ENDPOINT:-https://api.promptlayer.com/v1/traces}"
15
+ export PL_QUEUE_DRAIN_LIMIT="${PROMPTLAYER_QUEUE_DRAIN_LIMIT:-10}"
16
+ export PL_OTLP_CONNECT_TIMEOUT="${PROMPTLAYER_OTLP_CONNECT_TIMEOUT:-5}"
17
+ export PL_OTLP_MAX_TIME="${PROMPTLAYER_OTLP_MAX_TIME:-12}"
18
+ export PL_PLUGIN_VERSION="1.0.0"
19
+ PL_CC_VERSION="$(claude --version 2>/dev/null || echo 'unknown')"
20
+ export PL_CC_VERSION
21
+ export PL_USER_AGENT="promptlayer-claude-plugin/${PL_PLUGIN_VERSION} claude-code/${PL_CC_VERSION}"
22
+ PL_HOOKS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
23
+ export PL_HOOKS_DIR
24
+
25
+ mkdir -p "$(dirname "$PL_LOG_FILE")"
26
+ mkdir -p "$PL_SESSION_STATE_DIR"
27
+ mkdir -p "$PL_LOCK_DIR"
28
+ chmod 700 "$(dirname "$PL_LOG_FILE")" "$PL_SESSION_STATE_DIR" 2>/dev/null || true
29
+ chmod 700 "$PL_LOCK_DIR" 2>/dev/null || true
30
+
31
+ log() {
32
+ printf '%s [%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" "$2" >>"$PL_LOG_FILE"
33
+ }
34
+
35
+ debug() {
36
+ local v
37
+ v="$(echo "$PL_DEBUG" | tr '[:upper:]' '[:lower:]')"
38
+ if [[ "$v" == "1" || "$v" == "true" || "$v" == "yes" || "$v" == "on" ]]; then
39
+ log "DEBUG" "$1"
40
+ fi
41
+ }
42
+
43
+ tracing_enabled() {
44
+ local v
45
+ v="$(echo "${TRACE_TO_PROMPTLAYER:-true}" | tr '[:upper:]' '[:lower:]')"
46
+ [[ "$v" == "1" || "$v" == "true" || "$v" == "yes" || "$v" == "on" ]]
47
+ }
48
+
49
+ check_requirements() {
50
+ local cmd
51
+ for cmd in jq curl uuidgen python3; do
52
+ if ! command -v "$cmd" >/dev/null 2>&1; then
53
+ log "ERROR" "Missing required command: $cmd"
54
+ return 1
55
+ fi
56
+ done
57
+ if [[ -z "$PL_API_KEY" ]]; then
58
+ log "ERROR" "PROMPTLAYER_API_KEY is not set"
59
+ return 1
60
+ fi
61
+ return 0
62
+ }
63
+
64
+ generate_trace_id() {
65
+ uuidgen | tr -d '-' | tr '[:upper:]' '[:lower:]'
66
+ }
67
+
68
+ generate_span_id() {
69
+ uuidgen | tr -d '-' | tr '[:upper:]' '[:lower:]' | cut -c1-16
70
+ }
71
+
72
+ parse_traceparent() {
73
+ local raw="${1:-}"
74
+ [[ -z "$raw" ]] && return 1
75
+
76
+ raw="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]')"
77
+
78
+ if [[ ! "$raw" =~ ^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})(-.+)?$ ]]; then
79
+ return 1
80
+ fi
81
+
82
+ local version="${BASH_REMATCH[1]}"
83
+ local trace_id="${BASH_REMATCH[2]}"
84
+ local parent_span_id="${BASH_REMATCH[3]}"
85
+ local trace_flags="${BASH_REMATCH[4]}"
86
+ local suffix="${BASH_REMATCH[5]:-}"
87
+
88
+ if [[ "$version" == "ff" ]]; then
89
+ return 1
90
+ fi
91
+ if [[ "$version" == "00" && -n "$suffix" ]]; then
92
+ return 1
93
+ fi
94
+ if [[ "$trace_id" == "00000000000000000000000000000000" ]]; then
95
+ return 1
96
+ fi
97
+ if [[ "$parent_span_id" == "0000000000000000" ]]; then
98
+ return 1
99
+ fi
100
+
101
+ printf '%s %s %s %s\n' "$version" "$trace_id" "$parent_span_id" "$trace_flags"
102
+ }
103
+
104
+ load_initial_trace_context() {
105
+ PL_INITIAL_TRACEPARENT_VERSION=""
106
+ PL_INITIAL_TRACE_ID=""
107
+ PL_INITIAL_PARENT_SPAN_ID=""
108
+ PL_INITIAL_TRACE_FLAGS=""
109
+ PL_INITIAL_TRACE_CONTEXT_SOURCE="generated"
110
+
111
+ local raw="${PROMPTLAYER_TRACEPARENT:-}"
112
+ if [[ -z "$raw" ]]; then
113
+ return 0
114
+ fi
115
+
116
+ local parsed
117
+ if ! parsed="$(parse_traceparent "$raw")"; then
118
+ log "WARN" "Ignoring invalid PROMPTLAYER_TRACEPARENT"
119
+ return 1
120
+ fi
121
+
122
+ read -r PL_INITIAL_TRACEPARENT_VERSION PL_INITIAL_TRACE_ID PL_INITIAL_PARENT_SPAN_ID PL_INITIAL_TRACE_FLAGS <<<"$parsed"
123
+ PL_INITIAL_TRACE_CONTEXT_SOURCE="external_traceparent"
124
+ return 0
125
+ }
126
+
127
+ normalize_hex_id() {
128
+ local raw="$1"
129
+ local expected_len="$2"
130
+ local fallback="$3"
131
+ local label="$4"
132
+
133
+ local clean
134
+ clean="$(echo "$raw" | tr -cd '[:xdigit:]' | tr '[:upper:]' '[:lower:]')"
135
+ if [[ -z "$clean" ]]; then
136
+ clean="$fallback"
137
+ fi
138
+
139
+ if ((${#clean} > expected_len)); then
140
+ clean="${clean:0:expected_len}"
141
+ elif ((${#clean} < expected_len)); then
142
+ clean="$(printf "%-${expected_len}s" "$clean" | tr ' ' '0')"
143
+ fi
144
+
145
+ if [[ "$clean" != "$raw" ]]; then
146
+ log "WARN" "Normalized $label from '$raw' to '$clean'"
147
+ fi
148
+
149
+ echo "$clean"
150
+ }
151
+
152
+ hex_to_base64() {
153
+ local hex="$1"
154
+ python3 "$PL_HOOKS_DIR/hook_utils.py" hex_to_base64 "$hex"
155
+ }
156
+
157
+ now_ns() {
158
+ python3 "$PL_HOOKS_DIR/hook_utils.py" now_ns
159
+ }
160
+
161
+ session_state_file() {
162
+ echo "$PL_SESSION_STATE_DIR/$1.json"
163
+ }
164
+
165
+ acquire_session_lock() {
166
+ local sid="$1"
167
+ [[ -z "$sid" ]] && return 1
168
+
169
+ local lock_dir="$PL_LOCK_DIR/$sid.lock"
170
+ local attempts=0
171
+ while ! mkdir "$lock_dir" 2>/dev/null; do
172
+ attempts=$((attempts + 1))
173
+ if ((attempts >= 250)); then
174
+ log "ERROR" "Timed out waiting for session lock session_id=$sid"
175
+ return 1
176
+ fi
177
+ sleep 0.02
178
+ done
179
+
180
+ export PL_HELD_SESSION_LOCK="$lock_dir"
181
+ return 0
182
+ }
183
+
184
+ release_session_lock() {
185
+ local lock_dir="${PL_HELD_SESSION_LOCK:-}"
186
+ if [[ -n "$lock_dir" ]]; then
187
+ rmdir "$lock_dir" 2>/dev/null || rm -rf "$lock_dir"
188
+ unset PL_HELD_SESSION_LOCK
189
+ fi
190
+ return 0
191
+ }
192
+
193
+ acquire_queue_lock() {
194
+ local lock_dir="$PL_LOCK_DIR/queue.lock"
195
+ local attempts=0
196
+ while ! mkdir "$lock_dir" 2>/dev/null; do
197
+ attempts=$((attempts + 1))
198
+ if ((attempts >= 250)); then
199
+ log "ERROR" "Timed out waiting for queue lock"
200
+ return 1
201
+ fi
202
+ sleep 0.02
203
+ done
204
+
205
+ export PL_HELD_QUEUE_LOCK="$lock_dir"
206
+ return 0
207
+ }
208
+
209
+ release_queue_lock() {
210
+ local lock_dir="${PL_HELD_QUEUE_LOCK:-}"
211
+ if [[ -n "$lock_dir" ]]; then
212
+ rmdir "$lock_dir" 2>/dev/null || rm -rf "$lock_dir"
213
+ unset PL_HELD_QUEUE_LOCK
214
+ fi
215
+ return 0
216
+ }
217
+
218
+ get_session_state() {
219
+ local sid="$1"
220
+ local key="$2"
221
+ local f
222
+ f="$(session_state_file "$sid")"
223
+ if [[ -f "$f" ]]; then
224
+ jq -r ".${key} // empty" "$f" 2>/dev/null || true
225
+ fi
226
+ }
227
+
228
+ set_session_state() {
229
+ local sid="$1"
230
+ local key="$2"
231
+ local val="$3"
232
+ local f
233
+ f="$(session_state_file "$sid")"
234
+ local current
235
+ current='{}'
236
+ if [[ -f "$f" ]]; then
237
+ current="$(cat "$f")"
238
+ fi
239
+ echo "$current" | jq --arg k "$key" --arg v "$val" '.[$k] = $v' >"$f"
240
+ }
241
+
242
+ ensure_session_initialized() {
243
+ local sid="$1"
244
+ local requested_start_ns="${2:-}"
245
+ [[ -z "$sid" ]] && return 1
246
+
247
+ local trace_id session_span_id session_parent_span_id session_start_ns init_source pending_tool_calls
248
+ trace_id="$(get_session_state "$sid" trace_id)"
249
+ session_span_id="$(get_session_state "$sid" session_span_id)"
250
+ session_parent_span_id="$(get_session_state "$sid" session_parent_span_id)"
251
+ session_start_ns="$(get_session_state "$sid" session_start_ns)"
252
+ init_source="$(get_session_state "$sid" session_init_source)"
253
+ pending_tool_calls="$(get_session_state "$sid" pending_tool_calls)"
254
+
255
+ # Normal path: SessionStart already created state.
256
+ if [[ -n "$trace_id" && -n "$session_span_id" ]]; then
257
+ if [[ -z "$session_start_ns" ]]; then
258
+ [[ -z "$requested_start_ns" ]] && requested_start_ns="$(now_ns)"
259
+ set_session_state "$sid" session_start_ns "$requested_start_ns"
260
+ fi
261
+ if [[ -z "$init_source" ]]; then
262
+ set_session_state "$sid" session_init_source "unknown"
263
+ fi
264
+ if [[ -z "$pending_tool_calls" ]]; then
265
+ set_session_state "$sid" pending_tool_calls "[]"
266
+ fi
267
+ if [[ -z "$session_parent_span_id" ]]; then
268
+ set_session_state "$sid" session_parent_span_id ""
269
+ fi
270
+ if [[ -z "$(get_session_state "$sid" session_traceparent_version)" ]]; then
271
+ set_session_state "$sid" session_traceparent_version ""
272
+ fi
273
+ if [[ -z "$(get_session_state "$sid" session_trace_flags)" ]]; then
274
+ set_session_state "$sid" session_trace_flags ""
275
+ fi
276
+ if [[ -z "$(get_session_state "$sid" trace_context_source)" ]]; then
277
+ set_session_state "$sid" trace_context_source "generated"
278
+ fi
279
+ return 0
280
+ fi
281
+
282
+ # Fallback path for SDK environments that do not surface SessionStart.
283
+ [[ -z "$requested_start_ns" ]] && requested_start_ns="$(now_ns)"
284
+ load_initial_trace_context || true
285
+ [[ -z "$trace_id" ]] && trace_id="${PL_INITIAL_TRACE_ID:-}"
286
+ [[ -z "$trace_id" ]] && trace_id="$(generate_trace_id)"
287
+ [[ -z "$session_span_id" ]] && session_span_id="$(generate_span_id)"
288
+
289
+ set_session_state "$sid" trace_id "$trace_id"
290
+ set_session_state "$sid" session_span_id "$session_span_id"
291
+ set_session_state "$sid" session_parent_span_id "${PL_INITIAL_PARENT_SPAN_ID:-}"
292
+ set_session_state "$sid" session_start_ns "$requested_start_ns"
293
+ set_session_state "$sid" current_turn_start_ns ""
294
+ set_session_state "$sid" pending_tool_calls "[]"
295
+ set_session_state "$sid" session_init_source "lazy_init"
296
+ set_session_state "$sid" session_traceparent_version "${PL_INITIAL_TRACEPARENT_VERSION:-}"
297
+ set_session_state "$sid" session_trace_flags "${PL_INITIAL_TRACE_FLAGS:-}"
298
+ set_session_state "$sid" trace_context_source "${PL_INITIAL_TRACE_CONTEXT_SOURCE:-generated}"
299
+
300
+ log "INFO" "Session initialized lazily session_id=$sid trace_id=$trace_id"
301
+ }
302
+
303
+ post_otlp_payload_file() {
304
+ local payload_file="$1"
305
+ local status response_file
306
+ response_file="$(mktemp "${TMPDIR:-/tmp}/pl-otlp-response.XXXXXX")"
307
+ status="$(curl -sS -o "$response_file" -w "%{http_code}" -X POST \
308
+ -H "Content-Type: application/json" \
309
+ -H "X-Api-Key: $PL_API_KEY" \
310
+ -H "User-Agent: $PL_USER_AGENT" \
311
+ --connect-timeout "$PL_OTLP_CONNECT_TIMEOUT" \
312
+ --max-time "$PL_OTLP_MAX_TIME" \
313
+ "$PL_OTLP_ENDPOINT" \
314
+ -d @"$payload_file" || true)"
315
+
316
+ if [[ "$status" != "200" ]]; then
317
+ log "ERROR" "Failed OTLP export status=$status"
318
+ rm -f "$response_file"
319
+ return 1
320
+ fi
321
+
322
+ # OTLP JSON can return 200 with partialSuccess and rejected spans.
323
+ # Treat this as non-retryable (same payload is likely to be rejected again).
324
+ if command -v jq >/dev/null 2>&1; then
325
+ local rejected
326
+ rejected="$(jq -r '.partialSuccess.rejectedSpans // 0' "$response_file" 2>/dev/null || echo 0)"
327
+ if [[ "$rejected" != "0" ]]; then
328
+ local message
329
+ message="$(jq -r '.partialSuccess.errorMessage // "Unknown rejection"' "$response_file" 2>/dev/null || true)"
330
+ log "ERROR" "OTLP partial success: rejected_spans=$rejected message=$message"
331
+ rm -f "$response_file"
332
+ return 2
333
+ fi
334
+ fi
335
+
336
+ rm -f "$response_file"
337
+ return 0
338
+ }
339
+
340
+ append_to_otlp_queue_file() {
341
+ local payload_file="$1"
342
+ acquire_queue_lock || return 1
343
+ # Compact to single line to preserve ndjson format
344
+ jq -c '.' "$payload_file" >>"$PL_QUEUE_FILE"
345
+ chmod 600 "$PL_QUEUE_FILE" 2>/dev/null || true
346
+ release_queue_lock
347
+ return 0
348
+ }
349
+
350
+ drain_otlp_queue() {
351
+ if [[ ! -s "$PL_QUEUE_FILE" ]]; then
352
+ return 0
353
+ fi
354
+
355
+ local drain_limit="$PL_QUEUE_DRAIN_LIMIT"
356
+ if [[ ! "$drain_limit" =~ ^[0-9]+$ ]]; then
357
+ drain_limit=10
358
+ fi
359
+ if ((drain_limit <= 0)); then
360
+ return 0
361
+ fi
362
+
363
+ acquire_queue_lock || return 0
364
+
365
+ local -a queue_payloads
366
+ mapfile -t queue_payloads <"$PL_QUEUE_FILE" || true
367
+
368
+ local total max_attempts i rc replayed dropped retryable_fail fail_index
369
+ total="${#queue_payloads[@]}"
370
+ if ((total == 0)); then
371
+ release_queue_lock
372
+ return 0
373
+ fi
374
+
375
+ max_attempts=$((total < drain_limit ? total : drain_limit))
376
+ replayed=0
377
+ dropped=0
378
+ retryable_fail=0
379
+ fail_index=-1
380
+
381
+ local queued_tmp
382
+ queued_tmp="$(mktemp "${TMPDIR:-/tmp}/pl-otlp-queued.XXXXXX")"
383
+
384
+ for ((i = 0; i < max_attempts; i++)); do
385
+ if [[ -z "${queue_payloads[$i]}" ]]; then
386
+ continue
387
+ fi
388
+
389
+ printf '%s' "${queue_payloads[$i]}" >"$queued_tmp"
390
+
391
+ if post_otlp_payload_file "$queued_tmp"; then
392
+ replayed=$((replayed + 1))
393
+ continue
394
+ else
395
+ rc=$?
396
+ fi
397
+
398
+ if [[ "$rc" == "2" ]]; then
399
+ dropped=$((dropped + 1))
400
+ continue
401
+ fi
402
+
403
+ retryable_fail=1
404
+ fail_index="$i"
405
+ break
406
+ done
407
+
408
+ rm -f "$queued_tmp"
409
+
410
+ local tmp remaining_start
411
+ tmp="$(mktemp "${TMPDIR:-/tmp}/pl-otlp-queue.XXXXXX")"
412
+ if ((retryable_fail == 1)); then
413
+ remaining_start="$fail_index"
414
+ else
415
+ remaining_start="$max_attempts"
416
+ fi
417
+
418
+ for ((i = remaining_start; i < total; i++)); do
419
+ printf '%s\n' "${queue_payloads[$i]}" >>"$tmp"
420
+ done
421
+
422
+ mv "$tmp" "$PL_QUEUE_FILE"
423
+ chmod 600 "$PL_QUEUE_FILE" 2>/dev/null || true
424
+ release_queue_lock
425
+
426
+ if ((replayed > 0)); then
427
+ log "INFO" "Drained queued OTLP payloads count=$replayed"
428
+ fi
429
+ if ((dropped > 0)); then
430
+ log "WARN" "Dropped non-retryable queued OTLP payloads count=$dropped"
431
+ fi
432
+
433
+ return 0
434
+ }
435
+
436
+ send_otlp_payload_file() {
437
+ local payload_file="$1"
438
+ local rc
439
+
440
+ drain_otlp_queue || true
441
+
442
+ if post_otlp_payload_file "$payload_file"; then
443
+ return 0
444
+ else
445
+ rc=$?
446
+ fi
447
+
448
+ if [[ "$rc" == "1" ]]; then
449
+ append_to_otlp_queue_file "$payload_file" || log "ERROR" "Failed to append OTLP payload to queue"
450
+ fi
451
+ return 1
452
+ }
453
+
454
+ kind_int_to_string() {
455
+ case "$1" in
456
+ 0) echo "SPAN_KIND_UNSPECIFIED" ;;
457
+ 1) echo "SPAN_KIND_INTERNAL" ;;
458
+ 2) echo "SPAN_KIND_SERVER" ;;
459
+ 3) echo "SPAN_KIND_CLIENT" ;;
460
+ 4) echo "SPAN_KIND_PRODUCER" ;;
461
+ 5) echo "SPAN_KIND_CONSUMER" ;;
462
+ *) echo "SPAN_KIND_UNSPECIFIED" ;;
463
+ esac
464
+ }
465
+
466
+ build_span_json() {
467
+ local trace_id="$1"
468
+ local span_id="$2"
469
+ local parent_span_id="$3"
470
+ local name="$4"
471
+ local kind="$5"
472
+ local start_ns="$6"
473
+ local end_ns="$7"
474
+ local attrs_json="$8"
475
+
476
+ # Convert integer kind to protobuf JSON enum string if needed
477
+ if [[ "$kind" =~ ^[0-9]+$ ]]; then
478
+ kind="$(kind_int_to_string "$kind")"
479
+ fi
480
+
481
+ trace_id="$(normalize_hex_id "$trace_id" 32 "$(generate_trace_id)" "trace_id")"
482
+ span_id="$(normalize_hex_id "$span_id" 16 "$(generate_span_id)" "span_id")"
483
+ if [[ -n "$parent_span_id" ]]; then
484
+ parent_span_id="$(normalize_hex_id "$parent_span_id" 16 "$(generate_span_id)" "parent_span_id")"
485
+ fi
486
+
487
+ local trace_id_b64 span_id_b64 parent_span_id_b64
488
+ trace_id_b64="$(hex_to_base64 "$trace_id")"
489
+ span_id_b64="$(hex_to_base64 "$span_id")"
490
+ parent_span_id_b64=""
491
+ if [[ -n "$parent_span_id" ]]; then
492
+ parent_span_id_b64="$(hex_to_base64 "$parent_span_id")"
493
+ fi
494
+
495
+ local span_json
496
+ span_json="$(jq -cn \
497
+ --arg trace_id "$trace_id_b64" \
498
+ --arg span_id "$span_id_b64" \
499
+ --arg parent_span_id "$parent_span_id_b64" \
500
+ --arg name "$name" \
501
+ --arg kind "$kind" \
502
+ --arg start "$start_ns" \
503
+ --arg end "$end_ns" \
504
+ --argjson attributes "$attrs_json" \
505
+ '{
506
+ traceId: $trace_id,
507
+ spanId: $span_id,
508
+ parentSpanId: (if $parent_span_id == "" then null else $parent_span_id end),
509
+ name: $name,
510
+ kind: $kind,
511
+ startTimeUnixNano: $start,
512
+ endTimeUnixNano: $end,
513
+ attributes: (
514
+ $attributes
515
+ | to_entries
516
+ | map(select(.value != null))
517
+ | map(
518
+ . as $kv
519
+ | {
520
+ key: $kv.key,
521
+ value: (
522
+ if ($kv.value | type) == "string" then
523
+ {stringValue: $kv.value}
524
+ elif ($kv.value | type) == "boolean" then
525
+ {boolValue: $kv.value}
526
+ elif ($kv.value | type) == "number" then
527
+ if ($kv.value | floor) == $kv.value then
528
+ {intValue: ($kv.value | tostring)}
529
+ else
530
+ {doubleValue: $kv.value}
531
+ end
532
+ else
533
+ {stringValue: ($kv.value | tojson)}
534
+ end
535
+ )
536
+ }
537
+ )
538
+ )
539
+ }')"
540
+
541
+ echo "$span_json"
542
+ }
543
+
544
+ emit_spans_batch_file() {
545
+ local spans_file="$1"
546
+ if [[ ! -s "$spans_file" ]]; then
547
+ return 0
548
+ fi
549
+
550
+ local payload_file
551
+ payload_file="$(mktemp "${TMPDIR:-/tmp}/pl-otlp-batch.XXXXXX")"
552
+ jq -cs '{resourceSpans:[{resource:{attributes:[{key:"service.name",value:{stringValue:"claude-code"}}]},scopeSpans:[{spans:.}]}]}' "$spans_file" >"$payload_file"
553
+ send_otlp_payload_file "$payload_file"
554
+ local rc=$?
555
+ rm -f "$payload_file"
556
+ return $rc
557
+ }
558
+
559
+ emit_span() {
560
+ local trace_id="$1"
561
+ local span_id="$2"
562
+ local parent_span_id="$3"
563
+ local name="$4"
564
+ local kind="$5"
565
+ local start_ns="$6"
566
+ local end_ns="$7"
567
+ local attrs_json="$8"
568
+
569
+ local span_json spans_file
570
+ span_json="$(build_span_json "$trace_id" "$span_id" "$parent_span_id" "$name" "$kind" "$start_ns" "$end_ns" "$attrs_json")" || return 1
571
+ spans_file="$(mktemp "${TMPDIR:-/tmp}/pl-otlp-span.XXXXXX")"
572
+ printf '%s\n' "$span_json" >"$spans_file"
573
+ emit_spans_batch_file "$spans_file"
574
+ local rc=$?
575
+ rm -f "$spans_file"
576
+ return $rc
577
+ }