@bugsnag/core-performance
Advanced tools
Comparing version 0.1.1 to 0.2.0
@@ -38,3 +38,4 @@ import { isNumber } from './validation.js'; | ||
} | ||
if (Number.isInteger(attribute)) { | ||
// 'bugsnag.sampling.p' must always be sent as a doubleValue | ||
if (key !== 'bugsnag.sampling.p' && Number.isInteger(attribute)) { | ||
return { key, value: { intValue: `${attribute}` } }; | ||
@@ -41,0 +42,0 @@ } |
@@ -1,5 +0,3 @@ | ||
import { spanToJson } from './span.js'; | ||
class BatchProcessor { | ||
constructor(delivery, configuration, resourceAttributeSource, clock, retryQueue, sampler, probabilityManager) { | ||
constructor(delivery, configuration, retryQueue, sampler, probabilityManager, encoder) { | ||
this.batch = []; | ||
@@ -9,7 +7,6 @@ this.timeout = null; | ||
this.configuration = configuration; | ||
this.resourceAttributeSource = resourceAttributeSource; | ||
this.clock = clock; | ||
this.retryQueue = retryQueue; | ||
this.sampler = sampler; | ||
this.probabilityManager = probabilityManager; | ||
this.encoder = encoder; | ||
this.flush = this.flush.bind(this); | ||
@@ -47,13 +44,3 @@ } | ||
} | ||
const resourceAttributes = await this.resourceAttributeSource(this.configuration); | ||
const payload = { | ||
resourceSpans: [ | ||
{ | ||
resource: { | ||
attributes: resourceAttributes.toJson() | ||
}, | ||
scopeSpans: [{ spans: batch }] | ||
} | ||
] | ||
}; | ||
const payload = await this.encoder.encode(batch); | ||
const batchTime = Date.now(); | ||
@@ -94,7 +81,7 @@ try { | ||
for (const span of this.batch) { | ||
if (span.samplingProbability < probability) { | ||
if (span.samplingProbability.raw < probability.raw) { | ||
span.samplingProbability = probability; | ||
} | ||
if (this.sampler.sample(span)) { | ||
batch.push(spanToJson(span, this.clock)); | ||
batch.push(span); | ||
} | ||
@@ -101,0 +88,0 @@ } |
import { BatchProcessor } from './batch-processor.js'; | ||
import { validateConfig } from './config.js'; | ||
import { TracePayloadEncoder } from './delivery.js'; | ||
import ProbabilityFetcher from './probability-fetcher.js'; | ||
@@ -8,2 +9,3 @@ import ProbabilityManager from './probability-manager.js'; | ||
import Sampler from './sampler.js'; | ||
import { validateSpanOptions, coreSpanOptionSchema } from './span.js'; | ||
import { DefaultSpanContextStorage } from './span-context.js'; | ||
@@ -16,4 +18,5 @@ import { SpanFactory } from './span-factory.js'; | ||
const spanContextStorage = options.spanContextStorage || new DefaultSpanContextStorage(options.backgroundingListener); | ||
let logger = options.schema.logger.defaultValue; | ||
const sampler = new Sampler(1.0); | ||
const spanFactory = new SpanFactory(processor, sampler, options.idGenerator, options.spanAttributesSource, options.clock, options.backgroundingListener, options.schema.logger.defaultValue, spanContextStorage); | ||
const spanFactory = new SpanFactory(processor, sampler, options.idGenerator, options.spanAttributesSource, options.clock, options.backgroundingListener, logger, spanContextStorage); | ||
const plugins = options.plugins(spanFactory, spanContextStorage); | ||
@@ -23,5 +26,5 @@ return { | ||
const configuration = validateConfig(config, options.schema); | ||
const delivery = options.deliveryFactory(configuration.apiKey, configuration.endpoint); | ||
ProbabilityManager.create(options.persistence, sampler, new ProbabilityFetcher(delivery)).then((manager) => { | ||
processor = new BatchProcessor(delivery, configuration, options.resourceAttributesSource, options.clock, new InMemoryQueue(delivery, configuration.retryQueueMaxSize), sampler, manager); | ||
const delivery = options.deliveryFactory(configuration.endpoint); | ||
ProbabilityManager.create(options.persistence, sampler, new ProbabilityFetcher(delivery, configuration.apiKey)).then((manager) => { | ||
processor = new BatchProcessor(delivery, configuration, new InMemoryQueue(delivery, configuration.retryQueueMaxSize), sampler, manager, new TracePayloadEncoder(options.clock, configuration, options.resourceAttributesSource)); | ||
// ensure all spans started before .start() are added to the batch | ||
@@ -38,3 +41,4 @@ for (const span of bufferingProcessor.spans) { | ||
}); | ||
spanFactory.configure(processor, configuration.logger); | ||
logger = configuration.logger; | ||
spanFactory.configure(processor, logger); | ||
}); | ||
@@ -46,3 +50,4 @@ for (const plugin of plugins) { | ||
startSpan: (name, spanOptions) => { | ||
const span = spanFactory.startSpan(name, spanOptions); | ||
const cleanOptions = validateSpanOptions(name, spanOptions, coreSpanOptionSchema, logger); | ||
const span = spanFactory.startSpan(cleanOptions.name, cleanOptions.options); | ||
return spanFactory.toPublicApi(span); | ||
@@ -49,0 +54,0 @@ }, |
@@ -0,1 +1,50 @@ | ||
import { spanToJson } from './span.js'; | ||
class TracePayloadEncoder { | ||
constructor(clock, configuration, resourceAttributeSource) { | ||
this.clock = clock; | ||
this.configuration = configuration; | ||
this.resourceAttributeSource = resourceAttributeSource; | ||
} | ||
async encode(spans) { | ||
const resourceAttributes = await this.resourceAttributeSource(this.configuration); | ||
const jsonSpans = Array(spans.length); | ||
for (let i = 0; i < spans.length; ++i) { | ||
jsonSpans[i] = spanToJson(spans[i], this.clock); | ||
} | ||
const deliveryPayload = { | ||
resourceSpans: [ | ||
{ | ||
resource: { attributes: resourceAttributes.toJson() }, | ||
scopeSpans: [{ spans: jsonSpans }] | ||
} | ||
] | ||
}; | ||
return { | ||
body: deliveryPayload, | ||
headers: { | ||
'Bugsnag-Api-Key': this.configuration.apiKey, | ||
'Content-Type': 'application/json', | ||
'Bugsnag-Span-Sampling': this.generateSamplingHeader(spans) | ||
} | ||
}; | ||
} | ||
generateSamplingHeader(spans) { | ||
if (spans.length === 0) { | ||
return '1:0'; | ||
} | ||
const spanCounts = Object.create(null); | ||
for (const span of spans) { | ||
const existingValue = spanCounts[span.samplingProbability.raw] || 0; | ||
spanCounts[span.samplingProbability.raw] = existingValue + 1; | ||
} | ||
const rawProbabilities = Object.keys(spanCounts); | ||
const pairs = Array(rawProbabilities.length); | ||
for (let i = 0; i < rawProbabilities.length; ++i) { | ||
const rawProbability = rawProbabilities[i]; | ||
pairs[i] = `${rawProbability}:${spanCounts[rawProbability]}`; | ||
} | ||
return pairs.join(';'); | ||
} | ||
} | ||
const retryCodes = new Set([402, 407, 408, 429]); | ||
@@ -12,2 +61,2 @@ function responseStateFromStatusCode(statusCode) { | ||
export { responseStateFromStatusCode }; | ||
export { TracePayloadEncoder, responseStateFromStatusCode }; |
@@ -5,3 +5,3 @@ export { ResourceAttributes, SpanAttributes, attributeToJson } from './attributes.js'; | ||
export { createClient, createNoopClient } from './core.js'; | ||
export { responseStateFromStatusCode } from './delivery.js'; | ||
export { TracePayloadEncoder, responseStateFromStatusCode } from './delivery.js'; | ||
export { SpanEvents } from './events.js'; | ||
@@ -11,7 +11,8 @@ export { InMemoryPersistence } from './persistence.js'; | ||
export { InMemoryQueue } from './retry-queue.js'; | ||
export { SpanInternal, spanToJson } from './span.js'; | ||
export { default as Sampler } from './sampler.js'; | ||
export { SpanInternal, coreSpanOptionSchema, spanToJson, validateSpanOptions } from './span.js'; | ||
export { DefaultSpanContextStorage, spanContextEquals } from './span-context.js'; | ||
export { SpanFactory } from './span-factory.js'; | ||
export { timeToNumber } from './time.js'; | ||
export { isBoolean, isLogger, isNumber, isObject, isPersistedProbabilty, isSpanContext, isString, isStringArray, isStringOrRegExpArray, isStringWithLength } from './validation.js'; | ||
export { isBoolean, isLogger, isNumber, isObject, isPersistedProbabilty, isSpanContext, isString, isStringArray, isStringOrRegExpArray, isStringWithLength, isTime } from './validation.js'; | ||
export { default as traceIdToSamplingRate } from './trace-id-to-sampling-rate.js'; |
// the time to wait before retrying a failed request | ||
const RETRY_MILLISECONDS = 30 * 1000; | ||
// the request body sent when fetching a new probability value; this is the | ||
// minimal body the server expects to receive | ||
const PROBABILITY_REQUEST = { resourceSpans: [] }; | ||
class ProbabilityFetcher { | ||
constructor(delivery) { | ||
constructor(delivery, apiKey) { | ||
this.delivery = delivery; | ||
this.payload = { | ||
body: { resourceSpans: [] }, | ||
headers: { | ||
'Bugsnag-Api-Key': apiKey, | ||
'Content-Type': 'application/json', | ||
'Bugsnag-Span-Sampling': '1.0:0' | ||
} | ||
}; | ||
} | ||
@@ -13,3 +18,3 @@ async getNewProbability() { | ||
while (true) { | ||
const response = await this.delivery.send(PROBABILITY_REQUEST); | ||
const response = await this.delivery.send(this.payload); | ||
// in theory this should always be present, but it's possible the request | ||
@@ -16,0 +21,0 @@ // fails or there's a bug on the server side causing it not to be returned |
@@ -53,4 +53,4 @@ const msInDay = 24 * 60 * 60000; | ||
let count = 0; | ||
for (let i = 0; i < payload.resourceSpans.length; ++i) { | ||
const scopeSpans = payload.resourceSpans[i].scopeSpans; | ||
for (let i = 0; i < payload.body.resourceSpans.length; ++i) { | ||
const scopeSpans = payload.body.resourceSpans[i].scopeSpans; | ||
for (let j = 0; j < scopeSpans.length; ++j) { | ||
@@ -57,0 +57,0 @@ count += scopeSpans[j].spans.length; |
@@ -35,6 +35,9 @@ // sampling rates are stored as a number between 0 and 2^32 - 1 (i.e. they are | ||
get spanProbability() { | ||
return this.scaledProbability; | ||
return { | ||
raw: this._probability, | ||
scaled: this.scaledProbability | ||
}; | ||
} | ||
sample(span) { | ||
return span.samplingRate <= span.samplingProbability; | ||
return span.samplingRate <= span.samplingProbability.scaled; | ||
} | ||
@@ -41,0 +44,0 @@ } |
@@ -26,4 +26,4 @@ import { SpanAttributes } from './attributes.js'; | ||
} | ||
startSpan(name, options = {}) { | ||
const safeStartTime = timeToNumber(this.clock, options ? options.startTime : undefined); | ||
startSpan(name, options) { | ||
const safeStartTime = timeToNumber(this.clock, options.startTime); | ||
const spanId = this.idGenerator.generate(64); | ||
@@ -33,3 +33,3 @@ // if the parentContext option is not set use the current context | ||
// we are starting a new root span | ||
const parentContext = options && (isSpanContext(options.parentContext) || options.parentContext === null) | ||
const parentContext = isSpanContext(options.parentContext) || options.parentContext === null | ||
? options.parentContext | ||
@@ -40,3 +40,3 @@ : this.spanContextStorage.current; | ||
const attributes = new SpanAttributes(this.spanAttributesSource()); | ||
if (options && typeof options.isFirstClass === 'boolean') { | ||
if (typeof options.isFirstClass === 'boolean') { | ||
attributes.set('bugsnag.span.first_class', options.isFirstClass); | ||
@@ -48,3 +48,3 @@ } | ||
this.openSpans.add(span); | ||
if (!options || options.makeCurrentContext !== false) { | ||
if (options.makeCurrentContext !== false) { | ||
this.spanContextStorage.push(span); | ||
@@ -51,0 +51,0 @@ } |
import { SpanEvents } from './events.js'; | ||
import traceIdToSamplingRate from './trace-id-to-sampling-rate.js'; | ||
import { isTime, isSpanContext, isBoolean, isObject } from './validation.js'; | ||
@@ -37,2 +38,4 @@ function spanToJson(span, clock) { | ||
this.endTime = endTime; | ||
let _samplingProbability = samplingProbability; | ||
this.attributes.set('bugsnag.sampling.p', _samplingProbability.raw); | ||
return { | ||
@@ -48,3 +51,9 @@ id: this.id, | ||
endTime, | ||
samplingProbability, | ||
get samplingProbability() { | ||
return _samplingProbability; | ||
}, | ||
set samplingProbability(samplingProbability) { | ||
_samplingProbability = samplingProbability; | ||
this.attributes.set('bugsnag.sampling.p', _samplingProbability.raw); | ||
}, | ||
parentSpanId: this.parentSpanId | ||
@@ -57,3 +66,57 @@ }; | ||
} | ||
const coreSpanOptionSchema = { | ||
startTime: { | ||
message: 'should be a number or Date', | ||
getDefaultValue: () => undefined, | ||
validate: isTime | ||
}, | ||
parentContext: { | ||
message: 'should be a SpanContext', | ||
getDefaultValue: () => undefined, | ||
validate: (value) => value === null || isSpanContext(value) | ||
}, | ||
makeCurrentContext: { | ||
message: 'should be true|false', | ||
getDefaultValue: () => undefined, | ||
validate: isBoolean | ||
}, | ||
isFirstClass: { | ||
message: 'should be true|false', | ||
getDefaultValue: () => undefined, | ||
validate: isBoolean | ||
} | ||
}; | ||
function validateSpanOptions(name, options, schema, logger) { | ||
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) { | ||
logger.warn(`Invalid span options${warnings}`); | ||
} | ||
return { name, options: cleanOptions }; | ||
} | ||
export { SpanInternal, spanToJson }; | ||
export { SpanInternal, coreSpanOptionSchema, spanToJson, validateSpanOptions }; |
@@ -1,5 +0,3 @@ | ||
import { type ResourceAttributeSource } from './attributes'; | ||
import { type Clock } from './clock'; | ||
import { type Configuration, type InternalConfiguration } from './config'; | ||
import { type Delivery } from './delivery'; | ||
import { type Delivery, type TracePayloadEncoder } from './delivery'; | ||
import { type Processor } from './processor'; | ||
@@ -14,10 +12,9 @@ import type ProbabilityManager from './probability-manager'; | ||
private readonly configuration; | ||
private readonly resourceAttributeSource; | ||
private readonly clock; | ||
private readonly retryQueue; | ||
private readonly sampler; | ||
private readonly probabilityManager; | ||
private readonly encoder; | ||
private batch; | ||
private timeout; | ||
constructor(delivery: Delivery, configuration: InternalConfiguration<C>, resourceAttributeSource: ResourceAttributeSource<C>, clock: Clock, retryQueue: RetryQueue, sampler: ReadonlySampler, probabilityManager: MinimalProbabilityManager); | ||
constructor(delivery: Delivery, configuration: InternalConfiguration<C>, retryQueue: RetryQueue, sampler: ReadonlySampler, probabilityManager: MinimalProbabilityManager, encoder: TracePayloadEncoder<C>); | ||
private stop; | ||
@@ -24,0 +21,0 @@ private start; |
@@ -1,5 +0,7 @@ | ||
import { type JsonAttribute } from './attributes'; | ||
import { type Configuration, type InternalConfiguration } from './config'; | ||
import { type Clock } from './clock'; | ||
import { type JsonAttribute, type ResourceAttributeSource } from './attributes'; | ||
import { type JsonEvent } from './events'; | ||
import { type Kind } from './span'; | ||
export type DeliveryFactory = (apiKey: string, endpoint: string) => Delivery; | ||
import { type Kind, type SpanEnded } from './span'; | ||
export type DeliveryFactory = (endpoint: string) => Delivery; | ||
export type ResponseState = 'success' | 'failure-discard' | 'failure-retryable'; | ||
@@ -11,3 +13,3 @@ interface Response { | ||
export interface Delivery { | ||
send: (payload: DeliveryPayload) => Promise<Response>; | ||
send: (payload: TracePayload) => Promise<Response>; | ||
} | ||
@@ -38,4 +40,21 @@ interface Resource { | ||
} | ||
export interface TracePayload { | ||
body: DeliveryPayload; | ||
headers: { | ||
'Bugsnag-Api-Key': string; | ||
'Content-Type': 'application/json'; | ||
'Bugsnag-Span-Sampling': string; | ||
'Bugsnag-Sent-At'?: string; | ||
}; | ||
} | ||
export declare class TracePayloadEncoder<C extends Configuration> { | ||
private readonly clock; | ||
private readonly configuration; | ||
private readonly resourceAttributeSource; | ||
constructor(clock: Clock, configuration: InternalConfiguration<C>, resourceAttributeSource: ResourceAttributeSource<C>); | ||
encode(spans: SpanEnded[]): Promise<TracePayload>; | ||
generateSamplingHeader(spans: SpanEnded[]): string; | ||
} | ||
export declare function responseStateFromStatusCode(statusCode: number): ResponseState; | ||
export {}; | ||
//# sourceMappingURL=delivery.d.ts.map |
@@ -13,2 +13,3 @@ export * from './attributes'; | ||
export * from './retry-queue'; | ||
export { default as Sampler } from './sampler'; | ||
export * from './span'; | ||
@@ -15,0 +16,0 @@ export * from './span-context'; |
import { type Delivery } from './delivery'; | ||
declare class ProbabilityFetcher { | ||
private readonly delivery; | ||
constructor(delivery: Delivery); | ||
private readonly payload; | ||
constructor(delivery: Delivery, apiKey: string); | ||
getNewProbability(): Promise<number>; | ||
@@ -6,0 +7,0 @@ private timeBetweenRetries; |
@@ -1,4 +0,4 @@ | ||
import { type Delivery, type DeliveryPayload } from './delivery'; | ||
import { type Delivery, type TracePayload } from './delivery'; | ||
export interface RetryQueue { | ||
add: (payload: DeliveryPayload, time: number) => void; | ||
add: (payload: TracePayload, time: number) => void; | ||
flush: () => Promise<void>; | ||
@@ -12,5 +12,5 @@ } | ||
constructor(delivery: Delivery, retryQueueMaxSize: number); | ||
add(payload: DeliveryPayload, time: number): void; | ||
add(payload: TracePayload, time: number): void; | ||
flush(): Promise<void>; | ||
} | ||
//# sourceMappingURL=retry-queue.d.ts.map |
@@ -8,3 +8,3 @@ import { type SpanAttributesSource } from './attributes'; | ||
import { type ReadonlySampler } from './sampler'; | ||
import { type Span, SpanInternal, type SpanOptions } from './span'; | ||
import { SpanInternal, type Span, type SpanOptions } from './span'; | ||
import { type SpanContextStorage } from './span-context'; | ||
@@ -23,3 +23,3 @@ export declare class SpanFactory { | ||
private onBackgroundStateChange; | ||
startSpan(name: string, options?: SpanOptions): SpanInternal; | ||
startSpan(name: string, options: SpanOptions): SpanInternal; | ||
configure(processor: Processor, logger: Logger): void; | ||
@@ -26,0 +26,0 @@ endSpan(span: SpanInternal, endTime: number): void; |
import { type SpanAttribute, type SpanAttributes } from './attributes'; | ||
import { type Clock } from './clock'; | ||
import { type Logger } from './config'; | ||
import { type DeliverySpan } from './delivery'; | ||
@@ -18,6 +19,10 @@ import { SpanEvents } from './events'; | ||
} | ||
declare const validSpanProbability: unique symbol; | ||
export type SpanProbability = number & { | ||
[validSpanProbability]: true; | ||
declare const validScaledProbability: unique symbol; | ||
export type ScaledProbability = number & { | ||
[validScaledProbability]: true; | ||
}; | ||
export interface SpanProbability { | ||
readonly scaled: ScaledProbability; | ||
readonly raw: number; | ||
} | ||
export interface SpanEnded { | ||
@@ -60,3 +65,15 @@ readonly id: string; | ||
} | ||
export interface SpanOption<T> { | ||
message: string; | ||
getDefaultValue: (value: unknown) => T | undefined; | ||
validate: (value: unknown) => value is T; | ||
} | ||
export interface InternalSpanOptions<O extends SpanOptions> { | ||
name: string; | ||
options: O; | ||
} | ||
export type SpanOptionSchema = Record<string, SpanOption<unknown>>; | ||
export declare const coreSpanOptionSchema: SpanOptionSchema; | ||
export declare function validateSpanOptions<O extends SpanOptions>(name: string, options: unknown, schema: SpanOptionSchema, logger: Logger): InternalSpanOptions<O>; | ||
export {}; | ||
//# sourceMappingURL=span.d.ts.map |
import { type Logger } from './config'; | ||
import { type PersistedProbability } from './persistence'; | ||
import { type SpanContext } from './span-context'; | ||
import { type Time } from './time'; | ||
export declare const isBoolean: (value: unknown) => value is boolean; | ||
@@ -14,2 +15,3 @@ export declare const isObject: (value: unknown) => value is Record<string, unknown>; | ||
export declare const isSpanContext: (value: unknown) => value is SpanContext; | ||
export declare function isTime(value: unknown): value is Time; | ||
//# sourceMappingURL=validation.d.ts.map |
@@ -22,3 +22,6 @@ const isBoolean = (value) => value === true || value === false; | ||
typeof value.isValid === 'function'; | ||
function isTime(value) { | ||
return isNumber(value) || value instanceof Date; | ||
} | ||
export { isBoolean, isLogger, isNumber, isObject, isPersistedProbabilty, isSpanContext, isString, isStringArray, isStringOrRegExpArray, isStringWithLength }; | ||
export { isBoolean, isLogger, isNumber, isObject, isPersistedProbabilty, isSpanContext, isString, isStringArray, isStringOrRegExpArray, isStringWithLength, isTime }; |
{ | ||
"name": "@bugsnag/core-performance", | ||
"version": "0.1.1", | ||
"version": "0.2.0", | ||
"description": "Core performance client", | ||
@@ -35,3 +35,3 @@ "keywords": [ | ||
], | ||
"gitHead": "1c7bc0b1e10b226d693273d37e81180b4b6c54cc" | ||
"gitHead": "01cf746ea73d61a88eb9351d68f70867cb2fbbef" | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
79180
1425