Skip to content

Commit

Permalink
feat(react-native): delegate first class spans to native SDK when att…
Browse files Browse the repository at this point in the history
…ached (#556)

* feat(react-native): add turbo module initialise method

avoid repeated calls to the native module by calling initialise once on module load and getting all of the native settings at once

* feat(react-native): delegate first class spans to native SDK

* feat(react-native): run onSpanEnd callbacks when processing native spans

* test(react-native): add span factory unit tests

* refactor: move onSpanEndCallback logic out of batch processor

* revert: undo initialise method

This partially reverts commit 0820748.

* test(react-native): update native integration e2e test

* fix(react-native): preserve sampling attribute for native Android spans

* refactor(core): configure span factory as early as possible and move early spans logic into span factory

* test(react-native): test custom attributes on native spans

* refactor(react-native): always use discard end time when discarding spans

* fix(react-native): fix platform check
  • Loading branch information
yousif-bugsnag authored Jan 7, 2025
1 parent a46b98f commit 7d25f48
Show file tree
Hide file tree
Showing 17 changed files with 483 additions and 143 deletions.
4 changes: 4 additions & 0 deletions packages/core/lib/attributes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ export class SpanAttributes {
Array.from(this.attributes).forEach(([key, value]) => { this.validateAttribute(key, value) })
return Array.from(this.attributes).map(([key, value]) => attributeToJson(key, value))
}

toObject () {
return Object.fromEntries(this.attributes)
}
}

export class ResourceAttributes extends SpanAttributes {
Expand Down
41 changes: 3 additions & 38 deletions packages/core/lib/batch-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,9 @@ import type ProbabilityManager from './probability-manager'
import type { Processor } from './processor'
import type { RetryQueue } from './retry-queue'
import type { ReadonlySampler } from './sampler'
import type { Span, SpanEnded } from './span'
import type { SpanEnded } from './span'

import { millisecondsToNanoseconds } from './clock'
import { spanEndedToSpan } from './span'

export type OnSpanEndCallback = (span: Span) => boolean | Promise<boolean>
export type OnSpanEndCallbacks = OnSpanEndCallback[]
import { runSpanEndCallbacks } from './span'

type MinimalProbabilityManager = Pick<ProbabilityManager, 'setProbability' | 'ensureFreshProbability'>

Expand Down Expand Up @@ -114,37 +110,6 @@ export class BatchProcessor<C extends Configuration> implements Processor {
await this.flushQueue
}

private async runCallbacks (span: Span): Promise<boolean> {
if (this.configuration.onSpanEnd) {
const callbackStartTime = performance.now()
let continueToBatch = true
for (const callback of this.configuration.onSpanEnd) {
try {
let result = callback(span)

// @ts-expect-error result may or may not be a promise
if (typeof result.then === 'function') {
result = await result
}

if (result === false) {
continueToBatch = false
break
}
} catch (err) {
this.configuration.logger.error('Error in onSpanEnd callback: ' + err)
}
}
if (continueToBatch) {
const duration = millisecondsToNanoseconds(performance.now() - callbackStartTime)
span.setAttribute('bugsnag.span.callbacks_duration', duration)
}
return continueToBatch
} else {
return true
}
}

private async prepareBatch (): Promise<SpanEnded[] | undefined> {
if (this.spans.length === 0) {
return
Expand All @@ -165,7 +130,7 @@ export class BatchProcessor<C extends Configuration> implements Processor {
if (this.sampler.sample(span)) {
// Run any callbacks that have been registered before batching
// as callbacks could cause the span to be discarded
const shouldAddToBatch = await this.runCallbacks(spanEndedToSpan(span))
const shouldAddToBatch = await runSpanEndCallbacks(span, this.configuration.logger, this.configuration.onSpanEnd)
if (shouldAddToBatch) batch.push(span)
}
}
Expand Down
5 changes: 4 additions & 1 deletion packages/core/lib/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { OnSpanEndCallbacks } from './batch-processor'
import {
ATTRIBUTE_ARRAY_LENGTH_LIMIT_DEFAULT,
ATTRIBUTE_COUNT_LIMIT_DEFAULT,
Expand All @@ -8,6 +7,7 @@ import {
ATTRIBUTE_STRING_VALUE_LIMIT_MAX
} from './custom-attribute-limits'
import type { Plugin } from './plugin'
import type { Span } from './span'
import { isLogger, isNumber, isObject, isOnSpanEndCallbacks, isPluginArray, isString, isStringArray, isStringWithLength } from './validation'

type SetTraceCorrelation = (traceId: string, spanId: string) => void
Expand Down Expand Up @@ -39,6 +39,9 @@ export interface Logger {
error: (message: string) => void
}

export type OnSpanEndCallback = (span: Span) => boolean | Promise<boolean>
export type OnSpanEndCallbacks = OnSpanEndCallback[]

export interface Configuration {
apiKey: string
endpoint?: string
Expand Down
16 changes: 6 additions & 10 deletions packages/core/lib/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import type { Persistence } from './persistence'
import type { Plugin } from './plugin'
import ProbabilityFetcher from './probability-fetcher'
import ProbabilityManager from './probability-manager'
import type { Processor } from './processor'
import { BufferingProcessor } from './processor'
import type { RetryQueueFactory } from './retry-queue'
import Sampler from './sampler'
Expand Down Expand Up @@ -55,15 +54,14 @@ export type BugsnagPerformance <C extends Configuration, T> = Client<C> & T

export function createClient<S extends CoreSchema, C extends Configuration, T> (options: ClientOptions<S, C, T>): BugsnagPerformance<C, T> {
const bufferingProcessor = new BufferingProcessor()
let processor: Processor = bufferingProcessor
const spanContextStorage = options.spanContextStorage || new DefaultSpanContextStorage(options.backgroundingListener)
let logger = options.schema.logger.defaultValue
const sampler = new Sampler(1.0)

const SpanFactoryClass = options.spanFactory || SpanFactory

const spanFactory = new SpanFactoryClass(
processor,
bufferingProcessor,
sampler,
options.idGenerator,
options.spanAttributesSource,
Expand Down Expand Up @@ -101,6 +99,8 @@ export function createClient<S extends CoreSchema, C extends Configuration, T> (

options.spanAttributesSource.configure(configuration)

spanFactory.configure(configuration)

const probabilityManagerPromise = configuration.samplingProbability === undefined
? ProbabilityManager.create(
options.persistence,
Expand All @@ -110,7 +110,7 @@ export function createClient<S extends CoreSchema, C extends Configuration, T> (
: FixedProbabilityManager.create(sampler, configuration.samplingProbability)

probabilityManagerPromise.then((manager: ProbabilityManager | FixedProbabilityManager) => {
processor = new BatchProcessor(
const batchProcessor = new BatchProcessor(
delivery,
configuration,
options.retryQueueFactory(delivery, configuration.retryQueueMaxSize),
Expand All @@ -119,17 +119,14 @@ export function createClient<S extends CoreSchema, C extends Configuration, T> (
new TracePayloadEncoder(options.clock, configuration, options.resourceAttributesSource)
)

// ensure all spans started before .start() are added to the batch
for (const span of bufferingProcessor.spans) {
processor.add(span)
}
spanFactory.reprocessEarlySpans(batchProcessor)

// 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 as BatchProcessor<C>).flush()
batchProcessor.flush()

// ensure we have a fresh probability value when returning to the
// foreground
Expand All @@ -139,7 +136,6 @@ export function createClient<S extends CoreSchema, C extends Configuration, T> (
})

logger = configuration.logger
spanFactory.configure(processor, configuration)
})

for (const plugin of configuration.plugins) {
Expand Down
39 changes: 27 additions & 12 deletions packages/core/lib/span-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import type { SpanAttribute, SpanAttributesLimits, SpanAttributesSource } from '
import { SpanAttributes } from './attributes'
import type { BackgroundingListener, BackgroundingListenerState } from './backgrounding-listener'
import type { Clock } from './clock'
import type { Configuration, InternalConfiguration, Logger } from './config'
import type { Configuration, InternalConfiguration, Logger, OnSpanEndCallbacks } from './config'
import { defaultSpanAttributeLimits } from './custom-attribute-limits'
import type { IdGenerator } from './id-generator'
import type { NetworkSpanOptions } from './network-span'
import type { Processor } from './processor'
import type { BufferingProcessor, Processor } from './processor'
import type { ReadonlySampler } from './sampler'
import type { InternalSpanOptions, ParentContext, Span, SpanOptionSchema, SpanOptions } from './span'
import { SpanInternal, coreSpanOptionSchema } from './span'
Expand All @@ -27,8 +27,9 @@ export class SpanFactory<C extends Configuration> {
private readonly spanAttributesSource: SpanAttributesSource<C>
protected readonly clock: Clock
private readonly spanContextStorage: SpanContextStorage
private logger: Logger
protected logger: Logger
private spanAttributeLimits: SpanAttributesLimits = defaultSpanAttributeLimits
protected onSpanEndCallbacks?: OnSpanEndCallbacks

private openSpans: WeakSet<SpanInternal> = new WeakSet<SpanInternal>()
private isInForeground: boolean = true
Expand Down Expand Up @@ -73,6 +74,9 @@ export class SpanFactory<C extends Configuration> {
: 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, safeStartTime, parentContext, options.isFirstClass, attributes)

Expand All @@ -98,10 +102,6 @@ export class SpanFactory<C extends Configuration> {
const parentSpanId = parentContext ? parentContext.id : undefined
const traceId = parentContext ? parentContext.traceId : this.idGenerator.generate(128)

if (typeof isFirstClass === 'boolean') {
attributes.set('bugsnag.span.first_class', isFirstClass)
}

return new SpanInternal(spanId, traceId, name, startTime, attributes, this.clock, parentSpanId)
}

Expand All @@ -117,14 +117,23 @@ export class SpanFactory<C extends Configuration> {
return spanInternal
}

configure (processor: Processor, configuration: InternalConfiguration<C>) {
this.processor = processor
configure (configuration: InternalConfiguration<C>) {
this.logger = configuration.logger
this.spanAttributeLimits = {
attributeArrayLengthLimit: configuration.attributeArrayLengthLimit,
attributeCountLimit: configuration.attributeCountLimit,
attributeStringValueLimit: configuration.attributeStringValueLimit
}
this.onSpanEndCallbacks = configuration.onSpanEnd
}

reprocessEarlySpans (batchProcessor: Processor) {
// ensure all spans in the buffering processor are added to the batch
for (const span of (this.processor as BufferingProcessor).spans) {
batchProcessor.add(span)
}

this.processor = batchProcessor
}

endSpan (
Expand All @@ -148,8 +157,7 @@ export class SpanFactory<C extends Configuration> {
// - they are already invalid
// - they have an explicit discard end time
if (untracked || !isValidSpan || endTime === DISCARD_END_TIME) {
// we still call end on the span so that it is no longer considered valid
span.end(endTime, this.sampler.spanProbability)
this.discardSpan(span)
return
}

Expand All @@ -159,9 +167,16 @@ export class SpanFactory<C extends Configuration> {
}

this.spanAttributesSource.requestAttributes(span)
this.sendForProcessing(span, endTime)
}

const spanEnded = span.end(endTime, this.sampler.spanProbability)
protected discardSpan (span: SpanInternal) {
// we still call end on the span so that it is no longer considered valid
span.end(DISCARD_END_TIME, this.sampler.spanProbability)
}

protected sendForProcessing (span: SpanInternal, endTime: number) {
const spanEnded = span.end(endTime, this.sampler.spanProbability)
if (this.sampler.sample(spanEnded)) {
this.processor.add(spanEnded)
}
Expand Down
32 changes: 32 additions & 0 deletions packages/core/lib/span.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { SpanAttribute, SpanAttributes } from './attributes'
import { millisecondsToNanoseconds } from './clock'
import type { Clock } from './clock'
import type { Logger, OnSpanEndCallbacks } from './config'
import type { DeliverySpan } from './delivery'
import { SpanEvents } from './events'
import type { SpanContext } from './span-context'
Expand Down Expand Up @@ -86,6 +88,36 @@ export function spanEndedToSpan (span: SpanEnded): Span {
}
}

export async function runSpanEndCallbacks (spanEnded: SpanEnded, logger: Logger, callbacks?: OnSpanEndCallbacks) {
if (!callbacks) return true

const span = spanEndedToSpan(spanEnded)
const callbackStartTime = performance.now()
let shouldSample = true
for (const callback of callbacks) {
try {
let result = callback(span)

// @ts-expect-error result may or may not be a promise
if (typeof result.then === 'function') {
result = await result
}

if (result === false) {
shouldSample = false
break
}
} catch (err) {
logger.error('Error in onSpanEnd callback: ' + err)
}
}
if (shouldSample) {
const duration = millisecondsToNanoseconds(performance.now() - callbackStartTime)
span.setAttribute('bugsnag.span.callbacks_duration', duration)
}
return shouldSample
}

export class SpanInternal implements SpanContext {
readonly id: string
readonly traceId: string
Expand Down
3 changes: 1 addition & 2 deletions packages/core/lib/validation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { OnSpanEndCallback } from './batch-processor'
import type { Configuration, Logger } from './config'
import type { Configuration, Logger, OnSpanEndCallback } from './config'
import type { PersistedProbability } from './persistence'
import type { Plugin } from './plugin'
import type { ParentContext } from './span'
Expand Down
Loading

0 comments on commit 7d25f48

Please sign in to comment.