UNPKG

dd-trace

Version:

Datadog APM tracing client for JavaScript

150 lines (126 loc) 4.36 kB
'use strict' const log = require('../log') const { encodeDeltaVarint, hashTargetingKey } = require('./encoding') const MAX_SERIAL_IDS = 200 const MAX_SUBJECTS = 10 const MAX_EXPERIMENTS_PER_SUBJECT = 20 const MAX_DEFAULTS = 5 const MAX_DEFAULT_VALUE_LENGTH = 64 /** * Manages feature flag enrichment state for a single root span. * Accumulates serial IDs, subjects, and defaults throughout the span's lifetime. */ class SpanEnrichmentState { constructor () { /** @type {Set<number>} */ this._serialIds = new Set() /** @type {Map<string, Set<number>>} hashed targeting key -> serial IDs */ this._subjects = new Map() /** @type {Map<string, string>} flag key -> runtime default value */ this._defaults = new Map() } /** * Add a serial ID from a flag evaluation. * * @param {number} serialId - The serial ID to add * @returns {boolean} True if added, false if limit reached */ addSerialId (serialId) { if (this._serialIds.size >= MAX_SERIAL_IDS) { log.debug('SpanEnrichment: MAX_SERIAL_IDS limit (%d) reached, dropping serialId %d', MAX_SERIAL_IDS, serialId) return false } this._serialIds.add(serialId) return true } /** * Add a subject (targeting key) with its associated serial ID. * Only called when doLog=true. * * @param {string} targetingKey - The targeting key (will be hashed) * @param {number} serialId - The serial ID associated with this evaluation * @returns {boolean} True if added, false if limit reached */ addSubject (targetingKey, serialId) { const hashedKey = hashTargetingKey(targetingKey) if (this._subjects.has(hashedKey)) { const subjectIds = this._subjects.get(hashedKey) if (subjectIds.size >= MAX_EXPERIMENTS_PER_SUBJECT) { log.debug('SpanEnrichment: MAX_EXPERIMENTS_PER_SUBJECT limit (%d) reached for subject', MAX_EXPERIMENTS_PER_SUBJECT) return false } subjectIds.add(serialId) return true } if (this._subjects.size >= MAX_SUBJECTS) { log.debug('SpanEnrichment: MAX_SUBJECTS limit (%d) reached, dropping subject', MAX_SUBJECTS) return false } this._subjects.set(hashedKey, new Set([serialId])) return true } /** * Add a default fallback for a flag not found in UFC. * * @param {string} flagKey - The flag key * @param {boolean|string|number|object} defaultValue - The default value used * @returns {boolean} True if added, false if limit reached */ addDefault (flagKey, defaultValue) { if (this._defaults.has(flagKey)) { return true } if (this._defaults.size >= MAX_DEFAULTS) { log.debug('SpanEnrichment: MAX_DEFAULTS limit (%d) reached, dropping flag %s', MAX_DEFAULTS, flagKey) return false } let valueStr = typeof defaultValue === 'object' && defaultValue !== null ? JSON.stringify(defaultValue) : String(defaultValue) if (valueStr.length > MAX_DEFAULT_VALUE_LENGTH) { valueStr = valueStr.slice(0, MAX_DEFAULT_VALUE_LENGTH) } this._defaults.set(flagKey, valueStr) return true } /** * Check if there is any enrichment data to add to the span. * Note: _subjects is not checked because addSubject() is never called without first * calling addSerialId(), so _subjects having data necessitates _serialIds having data. * * @returns {boolean} True if there is data to add */ hasData () { return this._serialIds.size > 0 || this._defaults.size > 0 } /** * Convert accumulated state to span tags. * * @returns {object} Object with ffe_flags_enc, ffe_subjects_enc, and ffe_runtime_defaults tags */ toSpanTags () { const tags = {} if (this._serialIds.size > 0) { tags.ffe_flags_enc = encodeDeltaVarint(this._serialIds) } if (this._subjects.size > 0) { const subjectsObj = Object.fromEntries( [...this._subjects].map(([key, ids]) => [key, encodeDeltaVarint(ids)]) ) tags.ffe_subjects_enc = JSON.stringify(subjectsObj) } if (this._defaults.size > 0) { tags.ffe_runtime_defaults = JSON.stringify(Object.fromEntries(this._defaults)) } return tags } } module.exports = { SpanEnrichmentState, MAX_SERIAL_IDS, MAX_SUBJECTS, MAX_EXPERIMENTS_PER_SUBJECT, MAX_DEFAULTS, MAX_DEFAULT_VALUE_LENGTH, }