Skip to content

Commit ca829fc

Browse files
committed
Correctly report sampling header
1 parent d969c36 commit ca829fc

File tree

15 files changed

+473
-203
lines changed

15 files changed

+473
-203
lines changed

packages/core/lib/batch-processor.ts

Lines changed: 10 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,38 @@
1-
import { type ResourceAttributeSource } from './attributes'
2-
import { type Clock } from './clock'
31
import { type Configuration, type InternalConfiguration } from './config'
4-
import { type Delivery, type DeliverySpan } from './delivery'
2+
import { type Delivery, type TracePayloadEncoder } from './delivery'
53
import { type Processor } from './processor'
64
import type ProbabilityManager from './probability-manager'
75
import { type RetryQueue } from './retry-queue'
86
import { type ReadonlySampler } from './sampler'
9-
import { spanToJson, type SpanEnded } from './span'
7+
import { type SpanEnded } from './span'
108

119
type MinimalProbabilityManager = Pick<ProbabilityManager, 'setProbability'>
1210

1311
export class BatchProcessor<C extends Configuration> implements Processor {
1412
private readonly delivery: Delivery
1513
private readonly configuration: InternalConfiguration<C>
16-
private readonly resourceAttributeSource: ResourceAttributeSource<C>
17-
private readonly clock: Clock
1814
private readonly retryQueue: RetryQueue
1915
private readonly sampler: ReadonlySampler
2016
private readonly probabilityManager: MinimalProbabilityManager
17+
private readonly encoder: TracePayloadEncoder<C>
2118

2219
private batch: SpanEnded[] = []
2320
private timeout: ReturnType<typeof setTimeout> | null = null
2421

2522
constructor (
2623
delivery: Delivery,
2724
configuration: InternalConfiguration<C>,
28-
resourceAttributeSource: ResourceAttributeSource<C>,
29-
clock: Clock,
3025
retryQueue: RetryQueue,
3126
sampler: ReadonlySampler,
32-
probabilityManager: MinimalProbabilityManager
27+
probabilityManager: MinimalProbabilityManager,
28+
encoder: TracePayloadEncoder<C>
3329
) {
3430
this.delivery = delivery
3531
this.configuration = configuration
36-
this.resourceAttributeSource = resourceAttributeSource
37-
this.clock = clock
3832
this.retryQueue = retryQueue
3933
this.sampler = sampler
4034
this.probabilityManager = probabilityManager
35+
this.encoder = encoder
4136
this.flush = this.flush.bind(this)
4237
}
4338

@@ -79,19 +74,7 @@ export class BatchProcessor<C extends Configuration> implements Processor {
7974
return
8075
}
8176

82-
const resourceAttributes = await this.resourceAttributeSource(this.configuration)
83-
84-
const payload = {
85-
resourceSpans: [
86-
{
87-
resource: {
88-
attributes: resourceAttributes.toJson()
89-
},
90-
scopeSpans: [{ spans: batch }]
91-
}
92-
]
93-
}
94-
77+
const payload = await this.encoder.encode(batch)
9578
const batchTime = Date.now()
9679

9780
try {
@@ -122,13 +105,13 @@ export class BatchProcessor<C extends Configuration> implements Processor {
122105
}
123106
}
124107

125-
private prepareBatch (): DeliverySpan[] | undefined {
108+
private prepareBatch (): SpanEnded[] | undefined {
126109
if (this.batch.length === 0) {
127110
return
128111
}
129112

130113
// update sampling values if necessary and re-sample
131-
const batch: DeliverySpan[] = []
114+
const batch: SpanEnded[] = []
132115
const probability = this.sampler.spanProbability
133116

134117
for (const span of this.batch) {
@@ -137,7 +120,7 @@ export class BatchProcessor<C extends Configuration> implements Processor {
137120
}
138121

139122
if (this.sampler.sample(span)) {
140-
batch.push(spanToJson(span, this.clock))
123+
batch.push(span)
141124
}
142125
}
143126

packages/core/lib/core.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { type BackgroundingListener } from './backgrounding-listener'
33
import { BatchProcessor } from './batch-processor'
44
import { type Clock } from './clock'
55
import { validateConfig, type Configuration, type CoreSchema } from './config'
6-
import { type DeliveryFactory } from './delivery'
6+
import { type DeliveryFactory, TracePayloadEncoder } from './delivery'
77
import { type IdGenerator } from './id-generator'
88
import { type Persistence } from './persistence'
99
import { type Plugin } from './plugin'
@@ -57,21 +57,20 @@ export function createClient<S extends CoreSchema, C extends Configuration> (opt
5757
start: (config: C | string) => {
5858
const configuration = validateConfig<S, C>(config, options.schema)
5959

60-
const delivery = options.deliveryFactory(configuration.apiKey, configuration.endpoint)
60+
const delivery = options.deliveryFactory(configuration.endpoint)
6161

6262
ProbabilityManager.create(
6363
options.persistence,
6464
sampler,
65-
new ProbabilityFetcher(delivery)
65+
new ProbabilityFetcher(delivery, configuration.apiKey)
6666
).then((manager: ProbabilityManager) => {
6767
processor = new BatchProcessor(
6868
delivery,
6969
configuration,
70-
options.resourceAttributesSource,
71-
options.clock,
7270
new InMemoryQueue(delivery, configuration.retryQueueMaxSize),
7371
sampler,
74-
manager
72+
manager,
73+
new TracePayloadEncoder(options.clock, configuration, options.resourceAttributesSource)
7574
)
7675

7776
// ensure all spans started before .start() are added to the batch

packages/core/lib/delivery.ts

Lines changed: 88 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
import { type JsonAttribute } from './attributes'
1+
import { type Configuration, type InternalConfiguration } from './config'
2+
import { type Clock } from './clock'
3+
import { type JsonAttribute, type ResourceAttributeSource } from './attributes'
24
import { type JsonEvent } from './events'
3-
import { type Kind } from './span'
5+
import { type Kind, type SpanEnded, spanToJson } from './span'
46

5-
export type DeliveryFactory = (apiKey: string, endpoint: string) => Delivery
7+
export type DeliveryFactory = (endpoint: string) => Delivery
68

79
export type ResponseState = 'success' | 'failure-discard' | 'failure-retryable'
810

@@ -12,7 +14,7 @@ interface Response {
1214
}
1315

1416
export interface Delivery {
15-
send: (payload: DeliveryPayload) => Promise<Response>
17+
send: (payload: TracePayload) => Promise<Response>
1618
}
1719

1820
interface Resource {
@@ -44,6 +46,88 @@ export interface DeliverySpan {
4446
events: JsonEvent[]
4547
}
4648

49+
export interface TracePayload {
50+
body: DeliveryPayload
51+
headers: {
52+
'Bugsnag-Api-Key': string
53+
'Content-Type': 'application/json'
54+
'Bugsnag-Span-Sampling': string
55+
// we don't add 'Bugsnag-Sent-At' in the TracePayloadEncoder so that retried
56+
// payloads get a new value each time delivery is attempted
57+
// therefore it's 'undefined' when passed to delivery, which adds a value
58+
// immediately before initiating the request
59+
'Bugsnag-Sent-At'?: string
60+
}
61+
}
62+
63+
export class TracePayloadEncoder<C extends Configuration> {
64+
private readonly clock: Clock
65+
private readonly configuration: InternalConfiguration<C>
66+
private readonly resourceAttributeSource: ResourceAttributeSource<C>
67+
68+
constructor (
69+
clock: Clock,
70+
configuration: InternalConfiguration<C>,
71+
resourceAttributeSource: ResourceAttributeSource<C>
72+
) {
73+
this.clock = clock
74+
this.configuration = configuration
75+
this.resourceAttributeSource = resourceAttributeSource
76+
}
77+
78+
async encode (spans: SpanEnded[]): Promise<TracePayload> {
79+
const resourceAttributes = await this.resourceAttributeSource(this.configuration)
80+
const jsonSpans = Array(spans.length)
81+
82+
for (let i = 0; i < spans.length; ++i) {
83+
jsonSpans[i] = spanToJson(spans[i], this.clock)
84+
}
85+
86+
const deliveryPayload: DeliveryPayload = {
87+
resourceSpans: [
88+
{
89+
resource: { attributes: resourceAttributes.toJson() },
90+
scopeSpans: [{ spans: jsonSpans }]
91+
}
92+
]
93+
}
94+
95+
return {
96+
body: deliveryPayload,
97+
headers: {
98+
'Bugsnag-Api-Key': this.configuration.apiKey,
99+
'Content-Type': 'application/json',
100+
'Bugsnag-Span-Sampling': this.generateSamplingHeader(spans)
101+
}
102+
}
103+
}
104+
105+
generateSamplingHeader (spans: SpanEnded[]): string {
106+
if (spans.length === 0) {
107+
return '1:0'
108+
}
109+
110+
const spanCounts: Record<string, number> = Object.create(null)
111+
112+
for (const span of spans) {
113+
const existingValue = spanCounts[span.samplingProbability.raw] || 0
114+
115+
spanCounts[span.samplingProbability.raw] = existingValue + 1
116+
}
117+
118+
const rawProbabilities = Object.keys(spanCounts)
119+
const pairs = Array(rawProbabilities.length)
120+
121+
for (let i = 0; i < rawProbabilities.length; ++i) {
122+
const rawProbability = rawProbabilities[i]
123+
124+
pairs[i] = `${rawProbability}:${spanCounts[rawProbability]}`
125+
}
126+
127+
return pairs.join(';')
128+
}
129+
}
130+
47131
const retryCodes = new Set([402, 407, 408, 429])
48132

49133
export function responseStateFromStatusCode (statusCode: number): ResponseState {

packages/core/lib/probability-fetcher.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,28 @@
1-
import { type Delivery } from './delivery'
1+
import { type Delivery, type TracePayload } from './delivery'
22

33
// the time to wait before retrying a failed request
44
const RETRY_MILLISECONDS = 30 * 1000
55

6-
// the request body sent when fetching a new probability value; this is the
7-
// minimal body the server expects to receive
8-
const PROBABILITY_REQUEST = { resourceSpans: [] }
9-
106
class ProbabilityFetcher {
117
private readonly delivery: Delivery
8+
private readonly payload: TracePayload
129

13-
constructor (delivery: Delivery) {
10+
constructor (delivery: Delivery, apiKey: string) {
1411
this.delivery = delivery
12+
this.payload = {
13+
body: { resourceSpans: [] },
14+
headers: {
15+
'Bugsnag-Api-Key': apiKey,
16+
'Content-Type': 'application/json',
17+
'Bugsnag-Span-Sampling': '1.0:0'
18+
}
19+
}
1520
}
1621

1722
async getNewProbability (): Promise<number> {
1823
// keep making requests until we get a new probability value from the server
1924
while (true) {
20-
const response = await this.delivery.send(PROBABILITY_REQUEST)
25+
const response = await this.delivery.send(this.payload)
2126

2227
// in theory this should always be present, but it's possible the request
2328
// fails or there's a bug on the server side causing it not to be returned

packages/core/lib/retry-queue.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import { type Delivery, type DeliveryPayload } from './delivery'
1+
import { type Delivery, type TracePayload } from './delivery'
22

33
export interface RetryQueue {
4-
add: (payload: DeliveryPayload, time: number) => void
4+
add: (payload: TracePayload, time: number) => void
55
flush: () => Promise<void>
66
}
77

88
interface PayloadWithTimestamp {
9-
payload: DeliveryPayload
9+
payload: TracePayload
1010
time: number
1111
}
1212

@@ -18,7 +18,7 @@ export class InMemoryQueue implements RetryQueue {
1818

1919
constructor (private delivery: Delivery, private retryQueueMaxSize: number) {}
2020

21-
add (payload: DeliveryPayload, time: number) {
21+
add (payload: TracePayload, time: number) {
2222
this.payloads.push({ payload, time })
2323

2424
let spanCount = this.payloads.reduce((count, { payload }) => count + countSpansInPayload(payload), 0)
@@ -68,11 +68,11 @@ export class InMemoryQueue implements RetryQueue {
6868
}
6969
}
7070

71-
function countSpansInPayload (payload: DeliveryPayload) {
71+
function countSpansInPayload (payload: TracePayload) {
7272
let count = 0
7373

74-
for (let i = 0; i < payload.resourceSpans.length; ++i) {
75-
const scopeSpans = payload.resourceSpans[i].scopeSpans
74+
for (let i = 0; i < payload.body.resourceSpans.length; ++i) {
75+
const scopeSpans = payload.body.resourceSpans[i].scopeSpans
7676

7777
for (let j = 0; j < scopeSpans.length; ++j) {
7878
count += scopeSpans[j].spans.length

0 commit comments

Comments
 (0)