1/**
2 * HTTP propagation helpers for Effect tracing context.
3 *
4 * This module converts Effect `Tracer.Span` values into outbound trace headers
5 * and decodes inbound propagation headers into `Tracer.ExternalSpan` parents.
6 * HTTP clients use it to continue the current span across outgoing requests, and
7 * server middleware uses it to parent request spans from upstream services.
8 *
9 * @stability unstable
10 * @since 4.0.0
11 */
12import * as Option from "../Option.ts"
13import * as Tracer from "../Tracer.ts"
14import * as Headers from "./Headers.ts"
15
16/**
17 * Function type for decoding tracing headers into an external span.
18 *
19 * **Details**
20 *
21 * Returns `Option.none` when the headers do not contain a supported or valid trace
22 * context.
23 *
24 * @stability unstable
25 * @category models
26 * @since 4.0.0
27 */
28export interface FromHeaders {
29 (headers: Headers.Headers): Option.Option<Tracer.ExternalSpan>
30}
31
32/**
33 * Encodes a span into HTTP trace propagation headers.
34 *
35 * **Details**
36 *
37 * The generated headers include both compact B3 (`b3`) and W3C `traceparent`
38 * formats.
39 *
40 * @stability unstable
41 * @category encoding
42 * @since 4.0.0
43 */
44export const toHeaders = (span: Tracer.Span): Headers.Headers =>
45 Headers.fromRecordUnsafe({
46 b3: `${span.traceId}-${span.spanId}-${span.sampled ? "1" : "0"}${
47 Option.match(span.parent, {
48 onNone: () => "",
49 onSome: (parent) => `-${parent.spanId}`
50 })
51 }`,
52 traceparent: `00-${span.traceId}-${span.spanId}-${span.sampled ? "01" : "00"}`
53 })
54
55/**
56 * Decodes an external span safely from HTTP trace propagation headers.
57 *
58 * **Details**
59 *
60 * W3C `traceparent` is tried first, followed by compact B3 (`b3`) and then
61 * multi-header B3 (`x-b3-*`).
62 *
63 * @stability unstable
64 * @category decoding
65 * @since 4.0.0
66 */
67export const fromHeaders = (headers: Headers.Headers): Option.Option<Tracer.ExternalSpan> => {
68 let span = w3c(headers)
69 if (Option.isSome(span)) {
70 return span
71 }
72 span = b3(headers)
73 if (Option.isSome(span)) {
74 return span
75 }
76 return xb3(headers)
77}
78
79/**
80 * Decodes an external span safely from the compact B3 `b3` header.
81 *
82 * **Details**
83 *
84 * Returns `Option.none` when the header is missing or does not contain trace and
85 * span identifiers.
86 *
87 * @stability unstable
88 * @category decoding
89 * @since 4.0.0
90 */
91export const b3: FromHeaders = (headers) => {
92 if (!("b3" in headers)) {
93 return Option.none()
94 }
95 const parts = headers["b3"].split("-")
96 if (parts.length < 2) {
97 return Option.none()
98 }
99 return Option.some(Tracer.externalSpan({
100 traceId: parts[0],
101 spanId: parts[1],
102 sampled: parts[2] ? parts[2] === "1" || parts[2] === "d" : true
103 }))
104}
105
106/**
107 * Decodes an external span safely from multi-header B3 propagation headers.
108 *
109 * **Details**
110 *
111 * The decoder reads `x-b3-traceid`, `x-b3-spanid`, and optional `x-b3-sampled`
112 * and `x-b3-flags` headers. Debug flags override the sampling decision.
113 *
114 * @stability unstable
115 * @category decoding
116 * @since 4.0.0
117 */
118export const xb3: FromHeaders = (headers) => {
119 if (!(headers["x-b3-traceid"]) || !(headers["x-b3-spanid"])) {
120 return Option.none()
121 }
122 return Option.some(Tracer.externalSpan({
123 traceId: headers["x-b3-traceid"],
124 spanId: headers["x-b3-spanid"],
125 sampled: headers["x-b3-flags"] === "1" ||
126 (headers["x-b3-sampled"] ? headers["x-b3-sampled"] === "1" || headers["x-b3-sampled"] === "true" : true)
127 }))
128}
129
130const w3cTraceId = /^[0-9a-f]{32}$/i
131const w3cSpanId = /^[0-9a-f]{16}$/i
132
133/**
134 * Decodes an external span safely from the W3C `traceparent` header.
135 *
136 * **Details**
137 *
138 * Only version `00` headers with valid trace and span identifiers are accepted.
139 *
140 * @stability unstable
141 * @category decoding
142 * @since 4.0.0
143 */
144export const w3c: FromHeaders = (headers) => {
145 if (!(headers["traceparent"])) {
146 return Option.none()
147 }
148 const parts = headers["traceparent"].split("-")
149 if (parts.length !== 4) {
150 return Option.none()
151 }
152 const [version, traceId, spanId, flags] = parts
153 switch (version) {
154 case "00": {
155 if (
156 w3cTraceId.test(traceId) === false || w3cSpanId.test(spanId) === false ||
157 traceId === "00000000000000000000000000000000" || spanId === "0000000000000000"
158 ) {
159 return Option.none()
160 }
161 return Option.some(Tracer.externalSpan({
162 traceId,
163 spanId,
164 sampled: (parseInt(flags, 16) & 1) === 1
165 }))
166 }
167 default: {
168 return Option.none()
169 }
170 }
171}