@bugsnag/core-performance
Version:
Core performance client
194 lines (191 loc) • 8.55 kB
JavaScript
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 };