UNPKG

@bugsnag/core-performance

Version:
194 lines (191 loc) 8.55 kB
import { SpanAttributes } from './attributes.js'; import { defaultSpanAttributeLimits } from './custom-attribute-limits.js'; import { SpanInternal, coreSpanOptionSchema } from './span.js'; import { timeToNumber } from './time.js'; import { isParentContext, isObject } from './validation.js'; const DISCARD_END_TIME = -1; class SpanFactory { constructor(processor, sampler, idGenerator, spanAttributesSource, clock, backgroundingListener, logger, spanContextStorage) { this.spanAttributeLimits = defaultSpanAttributeLimits; this.openSpans = new WeakSet(); this.isInForeground = true; this.onBackgroundStateChange = (state) => { this.isInForeground = state === 'in-foreground'; // clear all open spans regardless of the new background state // since spans are only valid if they start and end while the app is in the foreground this.openSpans = new WeakSet(); }; this.processor = processor; this.sampler = sampler; this.idGenerator = idGenerator; this.spanAttributesSource = spanAttributesSource; this.clock = clock; this.logger = logger; this.spanContextStorage = spanContextStorage; // this will fire immediately if the app is already backgrounded backgroundingListener.onStateChange(this.onBackgroundStateChange); } startSpan(name, options) { // if the parentContext option is not set use the current context // if parentContext is explicitly null, or there is no current context, // we are starting a new root span options.parentContext = isParentContext(options.parentContext) || options.parentContext === null ? options.parentContext : this.spanContextStorage.current; const attributes = new SpanAttributes(new Map(), this.spanAttributeLimits, name, this.logger); if (typeof options.isFirstClass === 'boolean') { attributes.set('bugsnag.span.first_class', options.isFirstClass); } const span = this.createSpanInternal(name, options, attributes); // don't track spans that are started while the app is backgrounded if (this.isInForeground) { this.openSpans.add(span); if (options.makeCurrentContext !== false) { this.spanContextStorage.push(span); } } this.onSpanStart(span); return span; } onSpanStart(spanInternal) { if (this.onSpanStartCallbacks) { const span = this.toPublicApi(spanInternal); for (const onSpanStart of this.onSpanStartCallbacks) { try { onSpanStart(span); } catch (err) { this.logger.error('Error in onSpanStart callback: ' + err); } } } } createSpanInternal(name, options, attributes) { const safeStartTime = timeToNumber(this.clock, options.startTime); const spanId = this.idGenerator.generate(64); const parentSpanId = options.parentContext ? options.parentContext.id : undefined; const traceId = options.parentContext ? options.parentContext.traceId : this.idGenerator.generate(128); return new SpanInternal(spanId, traceId, name, safeStartTime, attributes, this.clock, this.sampler.probability, parentSpanId); } startNetworkSpan(options) { const spanName = `[HTTP/${options.method.toUpperCase()}]`; const cleanOptions = this.validateSpanOptions(spanName, options); const spanInternal = this.startSpan(cleanOptions.name, { ...cleanOptions.options, makeCurrentContext: false }); spanInternal.setAttribute('bugsnag.span.category', 'network'); spanInternal.setAttribute('http.method', options.method); spanInternal.setAttribute('http.url', options.url); return spanInternal; } configure(configuration) { this.logger = configuration.logger; this.spanAttributesSource.configure(configuration); this.spanAttributeLimits = { attributeArrayLengthLimit: configuration.attributeArrayLengthLimit, attributeCountLimit: configuration.attributeCountLimit, attributeStringValueLimit: configuration.attributeStringValueLimit }; this.onSpanStartCallbacks = configuration.onSpanStart; this.onSpanEndCallbacks = configuration.onSpanEnd; } reprocessEarlySpans(batchProcessor) { // ensure all spans in the buffering processor are added to the batch for (const span of this.processor.spans) { batchProcessor.add(span); } this.processor = batchProcessor; } endSpan(span, endTime, additionalAttributes) { // remove the span from the context stack (this will also remove any invalid spans) this.spanContextStorage.pop(span); const untracked = !this.openSpans.delete(span); const isValidSpan = span.isValid(); // log a warning if the span is already invalid and is not being tracked if (untracked && !isValidSpan) { this.logger.warn('Attempted to end a Span which is no longer valid.'); } // spans should be discarded if: // - they are not tracked (i.e. discarded due to backgrounding) // - they are already invalid // - they have an explicit discard end time if (untracked || !isValidSpan || endTime === DISCARD_END_TIME) { this.discardSpan(span); return; } // Set any additional attributes for (const [key, value] of Object.entries(additionalAttributes || {})) { span.setAttribute(key, value); } this.spanAttributesSource.requestAttributes(span); this.sendForProcessing(span, endTime); } discardSpan(span) { // we still call end on the span so that it is no longer considered valid span.end(DISCARD_END_TIME, this.sampler.spanProbability); } sendForProcessing(span, endTime) { const spanEnded = span.end(endTime, this.sampler.spanProbability); if (this.sampler.sample(spanEnded)) { this.processor.add(spanEnded); } } toPublicApi(span) { return { get id() { return span.id; }, get traceId() { return span.traceId; }, get samplingRate() { return span.samplingRate; }, get samplingProbability() { return span.samplingProbability; }, get name() { return span.name; }, isValid: () => span.isValid(), setAttribute: (name, value) => { span.setCustomAttribute(name, value); }, end: (endTime) => { const safeEndTime = timeToNumber(this.clock, endTime); this.endSpan(span, safeEndTime); } }; } validateSpanOptions(name, options, schema = coreSpanOptionSchema) { let warnings = ''; const cleanOptions = {}; if (typeof name !== 'string') { warnings += `\n - name should be a string, got ${typeof name}`; name = String(name); } if (options !== undefined && !isObject(options)) { warnings += '\n - options is not an object'; } else { const spanOptions = options || {}; for (const option of Object.keys(schema)) { if (Object.prototype.hasOwnProperty.call(spanOptions, option) && spanOptions[option] !== undefined) { if (schema[option].validate(spanOptions[option])) { cleanOptions[option] = spanOptions[option]; } else { warnings += `\n - ${option} ${schema[option].message}, got ${typeof spanOptions[option]}`; cleanOptions[option] = schema[option].getDefaultValue(spanOptions[option]); } } else { cleanOptions[option] = schema[option].getDefaultValue(spanOptions[option]); } } } if (warnings.length > 0) { this.logger.warn(`Invalid span options${warnings}`); } return { name, options: cleanOptions }; } } export { DISCARD_END_TIME, SpanFactory };