dd-trace
Version:
Datadog APM tracing client for JavaScript
170 lines (140 loc) • 4.23 kB
JavaScript
'use strict'
// W3C Trace Context §3.3.1.2: max 32 list-members.
// https://www.w3.org/TR/trace-context/#tracestate-header-field-values
const MAX_LIST_MEMBERS = 32
const WHITESPACE = /[ \t]/
/**
* Parse a separator-delimited string into key/value entries.
*
* @param {string} value
* @param {string} fieldSeparator Between entries.
* @param {string} pairSeparator Between key and value within an entry.
* @param {boolean} rejectValueTabs Drop entries whose value contains an internal tab.
* @returns {[string, string][]} Entries in reverse of wire order.
*/
function parseEntries (value, fieldSeparator, pairSeparator, rejectValueTabs) {
const segments = value.split(fieldSeparator, MAX_LIST_MEMBERS)
// TODO: We should extract dd no matter at what position and move it to the front of the list.
// Extract up 31 additional entries.
const entries = []
for (let index = 0; index < segments.length; index++) {
const segment = segments[index]
const splitIndex = segment.indexOf(pairSeparator)
if (splitIndex === -1) continue
const key = segment.slice(0, splitIndex).trim()
if (!key || WHITESPACE.test(key)) continue
// W3C §3.3.1.3.2: value = 0*255(chr) nblk-chr; chr = %x20 / nblk-chr (no tab).
// Leading 0x20 is part of value; trailing whitespace is OWS.
const entryValue = segment.slice(splitIndex + 1).trimEnd()
if (!entryValue || rejectValueTabs && entryValue.includes('\t')) continue
entries.push([key, entryValue])
}
// Reverse so the Map's insertion order is reverse of wire order. `toString`
// prepends as it iterates, which yields the original wire order back.
entries.reverse()
return entries
}
function fromString (Type, value, fieldSeparator, pairSeparator, rejectValueTabs) {
if (typeof value !== 'string' || !value.length) {
return new Type()
}
return new Type(parseEntries(value, fieldSeparator, pairSeparator, rejectValueTabs))
}
function toString (map, pairSeparator, fieldSeparator) {
let result = ''
for (const [key, value] of map) {
if (result) {
result = `${fieldSeparator}${result}`
}
result = `${key}${pairSeparator}${value}${result}`
}
return result
}
class TraceStateData {
#map
changed = false
constructor (entries) {
this.#map = entries ? new Map(entries) : new Map()
}
set (key, value) {
if (this.#map.get(key) === value && (value !== undefined || this.#map.has(key))) return this
this.changed = true
this.#map.set(key, value)
return this
}
get (key) {
return this.#map.get(key)
}
delete (key) {
this.changed = true
return this.#map.delete(key)
}
clear () {
this.changed = true
this.#map.clear()
}
entries () {
return this.#map.entries()
}
[Symbol.iterator] () {
return this.#map[Symbol.iterator]()
}
get size () {
return this.#map.size
}
static fromString (value) {
return fromString(TraceStateData, value, ';', ':', false)
}
toString () {
return toString(this, ':', ';')
}
}
/**
* Pairs are stored in reverse of the serialized format to rely on set ordering
* new entries at the end to express update movement.
*/
class TraceState {
#map
constructor (entries) {
this.#map = entries ? new Map(entries) : new Map()
}
// Delete entries on update to ensure they're moved to the end of the list
set (key, value) {
if (this.#map.has(key)) this.#map.delete(key)
this.#map.set(key, value)
return this
}
get (key) {
return this.#map.get(key)
}
delete (key) {
return this.#map.delete(key)
}
[Symbol.iterator] () {
return this.#map[Symbol.iterator]()
}
get size () {
return this.#map.size
}
forVendor (vendor, handle) {
const data = this.#map.get(vendor)
const state = TraceStateData.fromString(data)
const result = handle(state)
if (state.changed) {
const value = state.toString()
if (value) {
this.set(vendor, value)
} else {
this.delete(vendor)
}
}
return result
}
static fromString (value) {
return fromString(TraceState, value, ',', '=', true)
}
toString () {
return toString(this, '=', ',')
}
}
module.exports = TraceState