@bugsnag/core-performance
Advanced tools
Comparing version 0.0.2 to 0.1.0-alpha.0
@@ -0,1 +1,3 @@ | ||
import { isNumber } from './validation.js'; | ||
class SpanAttributes { | ||
@@ -6,3 +8,3 @@ constructor(initialValues) { | ||
set(name, value) { | ||
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { | ||
if (typeof value === 'string' || typeof value === 'boolean' || isNumber(value)) { | ||
this.attributes.set(name, value); | ||
@@ -9,0 +11,0 @@ } |
import { spanToJson } from './span.js'; | ||
class BatchProcessor { | ||
constructor(delivery, configuration, resourceAttributeSource, clock, retryQueue, sampler) { | ||
constructor(delivery, configuration, resourceAttributeSource, clock, retryQueue, sampler, probabilityManager) { | ||
this.batch = []; | ||
this.timeout = null; | ||
this.delivery = delivery; | ||
@@ -11,4 +13,3 @@ this.configuration = configuration; | ||
this.sampler = sampler; | ||
this.batch = []; | ||
this.timeout = null; | ||
this.probabilityManager = probabilityManager; | ||
this.flush = this.flush.bind(this); | ||
@@ -46,2 +47,3 @@ } | ||
} | ||
const resourceAttributes = await this.resourceAttributeSource(this.configuration); | ||
const payload = { | ||
@@ -51,3 +53,3 @@ resourceSpans: [ | ||
resource: { | ||
attributes: this.resourceAttributeSource(this.configuration).toJson() | ||
attributes: resourceAttributes.toJson() | ||
}, | ||
@@ -62,3 +64,3 @@ scopeSpans: [{ spans: batch }] | ||
if (response.samplingProbability !== undefined) { | ||
this.sampler.probability = response.samplingProbability; | ||
this.probabilityManager.setProbability(response.samplingProbability); | ||
} | ||
@@ -65,0 +67,0 @@ switch (response.state) { |
@@ -1,2 +0,2 @@ | ||
import { isStringWithLength, isString, isLogger, isStringArray, isObject } from './validation.js'; | ||
import { isStringWithLength, isString, isLogger, isStringArray, isNumber, isObject } from './validation.js'; | ||
@@ -42,3 +42,3 @@ const schema = { | ||
message: 'should be a number between 0 and 1', | ||
validate: (value) => typeof value === 'number' && value >= 0 && value <= 1 | ||
validate: (value) => isNumber(value) && value >= 0 && value <= 1 | ||
} | ||
@@ -45,0 +45,0 @@ }; |
import { BatchProcessor } from './batch-processor.js'; | ||
import { validateConfig } from './config.js'; | ||
import ProbabilityFetcher from './probability-fetcher.js'; | ||
import ProbabilityManager from './probability-manager.js'; | ||
import { BufferingProcessor } from './processor.js'; | ||
import { InMemoryQueue } from './retry-queue.js'; | ||
import Sampler from './sampler.js'; | ||
import { SpanFactory } from './span.js'; | ||
import { timeToNumber } from './time.js'; | ||
import { DefaultSpanContextStorage } from './span-context.js'; | ||
import { SpanFactory } from './span-factory.js'; | ||
@@ -12,4 +14,5 @@ function createClient(options) { | ||
let processor = bufferingProcessor; | ||
const spanContextStorage = options.spanContextStorage || new DefaultSpanContextStorage(options.backgroundingListener); | ||
const sampler = new Sampler(1.0); | ||
const spanFactory = new SpanFactory(processor, sampler, options.idGenerator, options.spanAttributesSource, options.clock, options.backgroundingListener); | ||
const spanFactory = new SpanFactory(processor, sampler, options.idGenerator, options.spanAttributesSource, options.clock, options.backgroundingListener, options.schema.logger.defaultValue, spanContextStorage); | ||
const plugins = options.plugins(spanFactory); | ||
@@ -20,16 +23,17 @@ return { | ||
const delivery = options.deliveryFactory(configuration.apiKey, configuration.endpoint); | ||
sampler.initialise(configuration.samplingProbability, delivery); | ||
processor = new BatchProcessor(delivery, configuration, options.resourceAttributesSource, options.clock, new InMemoryQueue(delivery, configuration.retryQueueMaxSize), sampler); | ||
// ensure all spans started before .start() are added to the batch | ||
bufferingProcessor.spans.forEach(span => { | ||
processor.add(span); | ||
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); | ||
// ensure all spans started before .start() are added to the batch | ||
for (const span of bufferingProcessor.spans) { | ||
processor.add(span); | ||
} | ||
// register with the backgrounding listener - we do this in 'start' as | ||
// there's nothing to do if we're backgrounded before start is called | ||
// e.g. we can't trigger delivery until we have the apiKey and endpoint | ||
// from configuration | ||
options.backgroundingListener.onStateChange(state => { | ||
processor.flush(); | ||
}); | ||
spanFactory.configure(processor, configuration.logger); | ||
}); | ||
// register with the backgrounding listener - we do this in 'start' as | ||
// there's nothing to do if we're backgrounded before start is called | ||
// e.g. we can't trigger delivery until we have the apiKey and endpoint | ||
// from configuration | ||
options.backgroundingListener.onStateChange(state => { | ||
processor.flush(); | ||
}); | ||
spanFactory.updateProcessor(processor); | ||
for (const plugin of plugins) { | ||
@@ -41,8 +45,6 @@ plugin.configure(configuration); | ||
const span = spanFactory.startSpan(name, spanOptions); | ||
return { | ||
end: (endTime) => { | ||
const safeEndTime = timeToNumber(options.clock, endTime); | ||
spanFactory.endSpan(span, safeEndTime); | ||
} | ||
}; | ||
return spanFactory.toPublicApi(span); | ||
}, | ||
get currentSpanContext() { | ||
return spanContextStorage.current; | ||
} | ||
@@ -55,3 +57,4 @@ }; | ||
start: noop, | ||
startSpan: () => ({ end: noop }) | ||
startSpan: () => ({ id: '', traceId: '', end: noop, isValid: () => false }), | ||
currentSpanContext: undefined | ||
}; | ||
@@ -58,0 +61,0 @@ } |
@@ -10,5 +10,7 @@ export { ResourceAttributes, SpanAttributes, attributeToJson } from './attributes.js'; | ||
export { InMemoryQueue } from './retry-queue.js'; | ||
export { SpanFactory, SpanInternal, spanToJson } from './span.js'; | ||
export { SpanInternal, spanToJson } from './span.js'; | ||
export { DefaultSpanContextStorage, spanContextEquals } from './span-context.js'; | ||
export { SpanFactory } from './span-factory.js'; | ||
export { timeToNumber } from './time.js'; | ||
export { isLogger, isObject, isPersistedProbabilty, isString, isStringArray, isStringOrRegExpArray, isStringWithLength } from './validation.js'; | ||
export { isBoolean, isLogger, isNumber, isObject, isPersistedProbabilty, isSpanContext, isString, isStringArray, isStringOrRegExpArray, isStringWithLength } from './validation.js'; | ||
export { default as traceIdToSamplingRate } from './trace-id-to-sampling-rate.js'; |
@@ -1,3 +0,1 @@ | ||
const PROBABILITY_REFRESH_INTERVAL = 24 * 60 * 60000; // 24 hours in ms | ||
const PROBABILITY_REFRESH_RETRY_INTERVAL = 30000; // 30 seconds | ||
// sampling rates are stored as a number between 0 and 2^32 - 1 (i.e. they are | ||
@@ -11,22 +9,2 @@ // u32s) so we need to scale the probability value to match this range as they | ||
constructor(initialProbability) { | ||
this.fetchSamplingProbability = async () => { | ||
if (!this.delivery) | ||
return; | ||
try { | ||
const payload = { resourceSpans: [] }; | ||
const response = await this.delivery.send(payload); | ||
// if the response doesn't contain a valid probability | ||
// or the request failed, retry in 30 seconds | ||
if (response.samplingProbability !== undefined) { | ||
this.probability = response.samplingProbability; | ||
} | ||
else { | ||
this.resetTimer(true); | ||
} | ||
} | ||
catch (err) { | ||
// request failed - retry | ||
this.resetTimer(true); | ||
} | ||
}; | ||
// we could just do 'this.probability = initialProbability' but TypeScript | ||
@@ -47,4 +25,2 @@ // doesn't like that as it doesn't directly initialise these properties in | ||
this.scaledProbability = scaleProbabilityToMatchSamplingRate(probability); | ||
// reset the timer whenever we receive a new probability value | ||
this.resetTimer(); | ||
} | ||
@@ -63,21 +39,7 @@ /** | ||
} | ||
initialise(configuredProbability, delivery) { | ||
this._probability = configuredProbability; | ||
this.scaledProbability = scaleProbabilityToMatchSamplingRate(configuredProbability); | ||
this.delivery = delivery; | ||
// make an initial request for the probability value | ||
// when this completes a timer will be setup to make periodic requests | ||
this.fetchSamplingProbability(); | ||
} | ||
sample(span) { | ||
return span.samplingRate <= span.samplingProbability; | ||
} | ||
resetTimer(isRetry = false) { | ||
if (this.interval) { | ||
clearInterval(this.interval); | ||
} | ||
this.interval = setInterval(this.fetchSamplingProbability, isRetry ? PROBABILITY_REFRESH_RETRY_INTERVAL : PROBABILITY_REFRESH_INTERVAL); | ||
} | ||
} | ||
export { Sampler as default }; |
@@ -1,4 +0,2 @@ | ||
import { SpanAttributes } from './attributes.js'; | ||
import { SpanEvents } from './events.js'; | ||
import { timeToNumber } from './time.js'; | ||
import traceIdToSamplingRate from './trace-id-to-sampling-rate.js'; | ||
@@ -12,2 +10,3 @@ | ||
traceId: span.traceId, | ||
parentSpanId: span.parentSpanId, | ||
startTimeUnixNano: clock.toUnixTimestampNanoseconds(span.startTime), | ||
@@ -20,3 +19,3 @@ endTimeUnixNano: clock.toUnixTimestampNanoseconds(span.endTime), | ||
class SpanInternal { | ||
constructor(id, traceId, name, startTime, attributes) { | ||
constructor(id, traceId, name, startTime, attributes, parentSpanId) { | ||
this.kind = 3 /* Kind.Client */; // TODO: How do we define the initial Kind? | ||
@@ -26,2 +25,3 @@ this.events = new SpanEvents(); | ||
this.traceId = traceId; | ||
this.parentSpanId = parentSpanId; | ||
this.name = name; | ||
@@ -39,2 +39,3 @@ this.startTime = startTime; | ||
end(endTime, samplingProbability) { | ||
this.endTime = endTime; | ||
return { | ||
@@ -50,50 +51,11 @@ id: this.id, | ||
endTime, | ||
samplingProbability | ||
samplingProbability, | ||
parentSpanId: this.parentSpanId | ||
}; | ||
} | ||
} | ||
class SpanFactory { | ||
constructor(processor, sampler, idGenerator, spanAttributesSource, clock, backgroundingListener) { | ||
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 will fire immediately if the app is already backgrounded | ||
backgroundingListener.onStateChange(this.onBackgroundStateChange); | ||
isValid() { | ||
return this.endTime === undefined; | ||
} | ||
startSpan(name, options = {}) { | ||
const safeStartTime = timeToNumber(this.clock, options ? options.startTime : undefined); | ||
const spanId = this.idGenerator.generate(64); | ||
const traceId = this.idGenerator.generate(128); | ||
const attributes = new SpanAttributes(this.spanAttributesSource()); | ||
const span = new SpanInternal(spanId, traceId, name, safeStartTime, attributes); | ||
// don't track spans that are started while the app is backgrounded | ||
if (this.isInForeground) { | ||
this.openSpans.add(span); | ||
} | ||
return span; | ||
} | ||
updateProcessor(processor) { | ||
this.processor = processor; | ||
} | ||
endSpan(span, endTime) { | ||
// if the span doesn't exist here it shouldn't be processed | ||
if (!this.openSpans.delete(span)) | ||
return; | ||
const spanEnded = span.end(endTime, this.sampler.spanProbability); | ||
if (this.sampler.sample(spanEnded)) { | ||
this.processor.add(spanEnded); | ||
} | ||
} | ||
} | ||
export { SpanFactory, SpanInternal, spanToJson }; | ||
export { SpanInternal, spanToJson }; |
@@ -0,3 +1,5 @@ | ||
import { isNumber } from './validation.js'; | ||
function timeToNumber(clock, time) { | ||
if (typeof time === 'number') { | ||
if (isNumber(time)) { | ||
// no need to change anything - we want to store numbers anyway | ||
@@ -4,0 +6,0 @@ // we assume this is nanosecond precision |
@@ -14,3 +14,3 @@ import { type InternalConfiguration, type Configuration } from './config'; | ||
} | ||
export type ResourceAttributeSource<C extends Configuration> = (configuration: InternalConfiguration<C>) => ResourceAttributes; | ||
export type ResourceAttributeSource<C extends Configuration> = (configuration: InternalConfiguration<C>) => Promise<ResourceAttributes>; | ||
export interface JsonAttribute { | ||
@@ -17,0 +17,0 @@ key: string; |
@@ -6,15 +6,18 @@ import { type ResourceAttributeSource } from './attributes'; | ||
import { type Processor } from './processor'; | ||
import type ProbabilityManager from './probability-manager'; | ||
import { type RetryQueue } from './retry-queue'; | ||
import type Sampler from './sampler'; | ||
import { type ReadonlySampler } from './sampler'; | ||
import { type SpanEnded } from './span'; | ||
type MinimalProbabilityManager = Pick<ProbabilityManager, 'setProbability'>; | ||
export declare class BatchProcessor<C extends Configuration> implements Processor { | ||
private delivery; | ||
private configuration; | ||
private resourceAttributeSource; | ||
private clock; | ||
private retryQueue; | ||
private sampler; | ||
private readonly delivery; | ||
private readonly configuration; | ||
private readonly resourceAttributeSource; | ||
private readonly clock; | ||
private readonly retryQueue; | ||
private readonly sampler; | ||
private readonly probabilityManager; | ||
private batch; | ||
private timeout; | ||
constructor(delivery: Delivery, configuration: InternalConfiguration<C>, resourceAttributeSource: ResourceAttributeSource<C>, clock: Clock, retryQueue: RetryQueue, sampler: Sampler); | ||
constructor(delivery: Delivery, configuration: InternalConfiguration<C>, resourceAttributeSource: ResourceAttributeSource<C>, clock: Clock, retryQueue: RetryQueue, sampler: ReadonlySampler, probabilityManager: MinimalProbabilityManager); | ||
private stop; | ||
@@ -26,2 +29,3 @@ private start; | ||
} | ||
export {}; | ||
//# sourceMappingURL=batch-processor.d.ts.map |
@@ -7,7 +7,11 @@ import { type ResourceAttributeSource, type SpanAttributesSource } from './attributes'; | ||
import { type IdGenerator } from './id-generator'; | ||
import { type Persistence } from './persistence'; | ||
import { type Plugin } from './plugin'; | ||
import { SpanFactory, type Span, type SpanOptions } from './span'; | ||
import { type Span, type SpanOptions } from './span'; | ||
import { type SpanContext, type SpanContextStorage } from './span-context'; | ||
import { SpanFactory } from './span-factory'; | ||
export interface BugsnagPerformance<C extends Configuration> { | ||
start: (config: C | string) => void; | ||
startSpan: (name: string, options?: SpanOptions) => Span; | ||
readonly currentSpanContext: SpanContext | undefined; | ||
} | ||
@@ -23,2 +27,4 @@ export interface ClientOptions<S extends CoreSchema, C extends Configuration> { | ||
plugins: (spanFactory: SpanFactory) => Array<Plugin<C>>; | ||
persistence: Persistence; | ||
spanContextStorage?: SpanContextStorage; | ||
} | ||
@@ -25,0 +31,0 @@ export declare function createClient<S extends CoreSchema, C extends Configuration>(options: ClientOptions<S, C>): BugsnagPerformance<C>; |
@@ -31,2 +31,3 @@ import { type JsonAttribute } from './attributes'; | ||
traceId: string; | ||
parentSpanId?: string; | ||
startTimeUnixNano: string; | ||
@@ -33,0 +34,0 @@ endTimeUnixNano: string; |
@@ -14,2 +14,4 @@ export * from './attributes'; | ||
export * from './span'; | ||
export * from './span-context'; | ||
export * from './span-factory'; | ||
export * from './time'; | ||
@@ -16,0 +18,0 @@ export * from './validation'; |
@@ -7,2 +7,3 @@ export interface PersistedProbability { | ||
'bugsnag-sampling-probability': PersistedProbability; | ||
'bugsnag-anonymous-id': string; | ||
} | ||
@@ -9,0 +10,0 @@ export type PersistenceKey = keyof PersistencePayloadMap; |
@@ -1,7 +0,12 @@ | ||
import { type Delivery } from './delivery'; | ||
import { type SpanEnded, type SpanProbability } from './span'; | ||
interface ReadonlySampler { | ||
readonly probability: number; | ||
readonly spanProbability: SpanProbability; | ||
readonly sample: (span: SpanEnded) => boolean; | ||
} | ||
interface ReadWriteSampler extends ReadonlySampler { | ||
probability: number; | ||
} | ||
declare class Sampler { | ||
private _probability; | ||
private delivery?; | ||
private interval?; | ||
/** | ||
@@ -29,8 +34,6 @@ * The current probability scaled to match sampling rate | ||
get spanProbability(): SpanProbability; | ||
initialise(configuredProbability: number, delivery: Delivery): void; | ||
sample(span: SpanEnded): boolean; | ||
private fetchSamplingProbability; | ||
private resetTimer; | ||
} | ||
export default Sampler; | ||
export { type ReadonlySampler, type ReadWriteSampler }; | ||
//# sourceMappingURL=sampler.d.ts.map |
@@ -1,11 +0,8 @@ | ||
import { SpanAttributes, type SpanAttribute, type SpanAttributesSource } from './attributes'; | ||
import { type BackgroundingListener } from './backgrounding-listener'; | ||
import { type SpanAttribute, type SpanAttributes } from './attributes'; | ||
import { type Clock } from './clock'; | ||
import { type DeliverySpan } from './delivery'; | ||
import { SpanEvents } from './events'; | ||
import { type IdGenerator } from './id-generator'; | ||
import { type Processor } from './processor'; | ||
import type Sampler from './sampler'; | ||
import { type SpanContext } from './span-context'; | ||
import { type Time } from './time'; | ||
export interface Span { | ||
export interface Span extends SpanContext { | ||
end: (endTime?: Time) => void; | ||
@@ -36,7 +33,9 @@ } | ||
samplingProbability: SpanProbability; | ||
readonly parentSpanId?: string; | ||
} | ||
export declare function spanToJson(span: SpanEnded, clock: Clock): DeliverySpan; | ||
export declare class SpanInternal { | ||
private readonly id; | ||
private readonly traceId; | ||
export declare class SpanInternal implements SpanContext { | ||
readonly id: string; | ||
readonly traceId: string; | ||
private readonly parentSpanId?; | ||
private readonly startTime; | ||
@@ -47,26 +46,17 @@ private readonly samplingRate; | ||
private readonly attributes; | ||
private readonly name; | ||
constructor(id: string, traceId: string, name: string, startTime: number, attributes: SpanAttributes); | ||
name: string; | ||
private endTime?; | ||
constructor(id: string, traceId: string, name: string, startTime: number, attributes: SpanAttributes, parentSpanId?: string); | ||
addEvent(name: string, time: number): void; | ||
setAttribute(name: string, value: SpanAttribute): void; | ||
end(endTime: number, samplingProbability: SpanProbability): SpanEnded; | ||
isValid(): boolean; | ||
} | ||
export interface SpanOptions { | ||
startTime?: Time; | ||
makeCurrentContext?: boolean; | ||
parentContext?: SpanContext | null; | ||
isFirstClass?: boolean; | ||
} | ||
export declare class SpanFactory { | ||
private readonly idGenerator; | ||
private readonly spanAttributesSource; | ||
private processor; | ||
private readonly sampler; | ||
private readonly clock; | ||
private openSpans; | ||
private isInForeground; | ||
constructor(processor: Processor, sampler: Sampler, idGenerator: IdGenerator, spanAttributesSource: SpanAttributesSource, clock: Clock, backgroundingListener: BackgroundingListener); | ||
private onBackgroundStateChange; | ||
startSpan(name: string, options?: SpanOptions): SpanInternal; | ||
updateProcessor(processor: Processor): void; | ||
endSpan(span: SpanInternal, endTime: number): void; | ||
} | ||
export {}; | ||
//# sourceMappingURL=span.d.ts.map |
import { type Logger } from './config'; | ||
import { type PersistedProbability } from './persistence'; | ||
import { type SpanContext } from './span-context'; | ||
export declare const isBoolean: (value: unknown) => value is boolean; | ||
export declare const isObject: (value: unknown) => value is Record<string, unknown>; | ||
export declare const isNumber: (value: unknown) => value is number; | ||
export declare const isString: (value: unknown) => value is string; | ||
@@ -10,2 +13,3 @@ export declare const isStringWithLength: (value: unknown) => value is string; | ||
export declare function isPersistedProbabilty(value: unknown): value is PersistedProbability; | ||
export declare const isSpanContext: (value: unknown) => value is SpanContext; | ||
//# sourceMappingURL=validation.d.ts.map |
@@ -0,2 +1,4 @@ | ||
const isBoolean = (value) => value === true || value === false; | ||
const isObject = (value) => !!value && typeof value === 'object' && !Array.isArray(value); | ||
const isNumber = (value) => typeof value === 'number' && Number.isFinite(value) && !Number.isNaN(value); | ||
const isString = (value) => typeof value === 'string'; | ||
@@ -13,6 +15,10 @@ const isStringWithLength = (value) => isString(value) && value.length > 0; | ||
return isObject(value) && | ||
typeof value.value === 'number' && | ||
typeof value.time === 'number'; | ||
isNumber(value.value) && | ||
isNumber(value.time); | ||
} | ||
const isSpanContext = (value) => isObject(value) && | ||
typeof value.id === 'string' && | ||
typeof value.traceId === 'string' && | ||
typeof value.isValid === 'function'; | ||
export { isLogger, isObject, isPersistedProbabilty, isString, isStringArray, isStringOrRegExpArray, isStringWithLength }; | ||
export { isBoolean, isLogger, isNumber, isObject, isPersistedProbabilty, isSpanContext, isString, isStringArray, isStringOrRegExpArray, isStringWithLength }; |
{ | ||
"name": "@bugsnag/core-performance", | ||
"version": "0.0.2", | ||
"version": "0.1.0-alpha.0", | ||
"description": "Core performance client", | ||
@@ -23,2 +23,3 @@ "keywords": [ | ||
"build": "rollup --config", | ||
"build:cdn": "npm run build", | ||
"clean": "rm -rf dist/*" | ||
@@ -35,3 +36,3 @@ }, | ||
], | ||
"gitHead": "339a055dca4f3fcc17d9d6b4d2a73434495228d5" | ||
"gitHead": "e9a704d8984c0402e41e73510095b4f5708967e4" | ||
} |
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
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
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
71182
69
1263
1