From a2685ff18f089615d06dbedeb946074d072c5e5b Mon Sep 17 00:00:00 2001 From: Trent Mick Date: Thu, 15 Jan 2026 12:24:59 -0800 Subject: [PATCH 1/5] feat(sampler-composite): add ComposableAnnotatingSampler Refs: https://opentelemetry.io/docs/specs/otel/trace/sdk/#composableannotating --- .../sampler-composite/src/annotating.ts | 67 ++++++++++++++ .../packages/sampler-composite/src/index.ts | 1 + .../sampler-composite/test/annotating.test.ts | 88 +++++++++++++++++++ 3 files changed, 156 insertions(+) create mode 100644 experimental/packages/sampler-composite/src/annotating.ts create mode 100644 experimental/packages/sampler-composite/test/annotating.test.ts diff --git a/experimental/packages/sampler-composite/src/annotating.ts b/experimental/packages/sampler-composite/src/annotating.ts new file mode 100644 index 00000000000..cd3cf844c49 --- /dev/null +++ b/experimental/packages/sampler-composite/src/annotating.ts @@ -0,0 +1,67 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Attributes, Context, Link, SpanKind } from '@opentelemetry/api'; +import type { ComposableSampler, SamplingIntent } from './types'; + +class ComposableAnnotatingSampler implements ComposableSampler { + private readonly attributes: Attributes; + private readonly delegate: ComposableSampler; + private readonly description: string; + + constructor(attributes: Attributes, delegate: ComposableSampler) { + // Shallow copy `attributes` to avoid changes to the original object (at + // least top-level fields) impacting the sampler. Freeze the object so + // we can return it in `getSamplingIntent` without copying there. + this.attributes = Object.freeze({ ...attributes }); + this.delegate = delegate; + this.description = `ComposableAnnotatingSampler(attributes, delegate=${delegate})`; + } + + getSamplingIntent( + context: Context, + traceId: string, + spanName: string, + spanKind: SpanKind, + attributes: Attributes, + links: Link[] + ): SamplingIntent { + const intent = this.delegate.getSamplingIntent( + context, + traceId, + spanName, + spanKind, + attributes, + links + ); + if (!intent.attributes) { + intent.attributes = this.attributes; + } else { + intent.attributes = { ...intent.attributes, ...this.attributes }; + } + return intent; + } + + toString(): string { + return this.description; + } +} + +export function createComposableAnnotatingSampler( + attributes: Attributes, + delegate: ComposableSampler +): ComposableSampler { + return new ComposableAnnotatingSampler(attributes, delegate); +} diff --git a/experimental/packages/sampler-composite/src/index.ts b/experimental/packages/sampler-composite/src/index.ts index 91e68bf22e6..d36e0adfa46 100644 --- a/experimental/packages/sampler-composite/src/index.ts +++ b/experimental/packages/sampler-composite/src/index.ts @@ -18,5 +18,6 @@ export { createComposableAlwaysOffSampler } from './alwaysoff'; export { createComposableAlwaysOnSampler } from './alwayson'; export { createComposableTraceIDRatioBasedSampler } from './traceidratio'; export { createComposableParentThresholdSampler } from './parentthreshold'; +export { createComposableAnnotatingSampler } from './annotating'; export { createCompositeSampler } from './composite'; export type { ComposableSampler, SamplingIntent } from './types'; diff --git a/experimental/packages/sampler-composite/test/annotating.test.ts b/experimental/packages/sampler-composite/test/annotating.test.ts new file mode 100644 index 00000000000..74e6192ef44 --- /dev/null +++ b/experimental/packages/sampler-composite/test/annotating.test.ts @@ -0,0 +1,88 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as assert from 'assert'; + +import { context, SpanKind } from '@opentelemetry/api'; + +import { + createComposableAlwaysOnSampler, + createComposableAnnotatingSampler, +} from '../src'; + +describe('ComposableAnnotatingSampler', () => { + const alwaysOnSampler = createComposableAlwaysOnSampler(); + + it('should have a description', () => { + const sampler = createComposableAnnotatingSampler({}, alwaysOnSampler); + assert.strictEqual( + sampler.toString(), + 'ComposableAnnotatingSampler(attributes, delegate=ComposableAlwaysOnSampler)' + ); + }); + + it('should add attributes to SamplingIntent', () => { + const delegateIntent = alwaysOnSampler.getSamplingIntent( + context.active(), + 'unused', + 'span-name', + SpanKind.SERVER, + {}, + [] + ); + const sampler = createComposableAnnotatingSampler( + { foo: 'bar' }, + alwaysOnSampler + ); + const intent = sampler.getSamplingIntent( + context.active(), + 'unused', + 'span-name', + SpanKind.SERVER, + {}, + [] + ); + assert.strictEqual(intent.threshold, delegateIntent.threshold); + assert.strictEqual( + intent.thresholdReliable, + delegateIntent.thresholdReliable + ); + assert.deepStrictEqual(intent.attributes, { foo: 'bar' }); + }); + + it('should merge attributes', () => { + const sampler = createComposableAnnotatingSampler( + { foo: 'bar', spam: 'eggs' }, + createComposableAnnotatingSampler( + { foo: 'baz', wuz: 'here' }, + alwaysOnSampler + ) + ); + const intent = sampler.getSamplingIntent( + context.active(), + 'unused', + 'span-name', + SpanKind.SERVER, + {}, + [] + ); + assert.deepStrictEqual(intent.attributes, { + foo: 'bar', // outer annotating sampler wins + spam: 'eggs', + wuz: 'here' + }); + }); +}); From cf329d59caf5500b3618e0438ffd809988e1f6da Mon Sep 17 00:00:00 2001 From: Trent Mick Date: Thu, 15 Jan 2026 12:44:02 -0800 Subject: [PATCH 2/5] guard against the Annotating sampler *mutating* the delegate's possibly static SamplingIntent --- .../sampler-composite/src/alwaysoff.ts | 4 +-- .../sampler-composite/src/alwayson.ts | 4 +-- .../sampler-composite/src/annotating.ts | 26 ++++++++++--------- .../sampler-composite/src/traceidratio.ts | 8 +++--- .../sampler-composite/test/annotating.test.ts | 16 ++++++------ 5 files changed, 30 insertions(+), 28 deletions(-) diff --git a/experimental/packages/sampler-composite/src/alwaysoff.ts b/experimental/packages/sampler-composite/src/alwaysoff.ts index 2c41635cdee..58b44debf2d 100644 --- a/experimental/packages/sampler-composite/src/alwaysoff.ts +++ b/experimental/packages/sampler-composite/src/alwaysoff.ts @@ -16,10 +16,10 @@ import type { ComposableSampler, SamplingIntent } from './types'; import { INVALID_THRESHOLD } from './util'; -const intent: SamplingIntent = { +const intent: SamplingIntent = Object.freeze({ threshold: INVALID_THRESHOLD, thresholdReliable: false, -}; +}); class ComposableAlwaysOffSampler implements ComposableSampler { getSamplingIntent(): SamplingIntent { diff --git a/experimental/packages/sampler-composite/src/alwayson.ts b/experimental/packages/sampler-composite/src/alwayson.ts index 832d81e665b..e3a50d3c61a 100644 --- a/experimental/packages/sampler-composite/src/alwayson.ts +++ b/experimental/packages/sampler-composite/src/alwayson.ts @@ -16,10 +16,10 @@ import type { ComposableSampler, SamplingIntent } from './types'; import { MIN_THRESHOLD } from './util'; -const intent: SamplingIntent = { +const intent: SamplingIntent = Object.freeze({ threshold: MIN_THRESHOLD, thresholdReliable: true, -}; +}); class ComposableAlwaysOnSampler implements ComposableSampler { getSamplingIntent(): SamplingIntent { diff --git a/experimental/packages/sampler-composite/src/annotating.ts b/experimental/packages/sampler-composite/src/annotating.ts index cd3cf844c49..60cf5300ff8 100644 --- a/experimental/packages/sampler-composite/src/annotating.ts +++ b/experimental/packages/sampler-composite/src/annotating.ts @@ -17,17 +17,17 @@ import { Attributes, Context, Link, SpanKind } from '@opentelemetry/api'; import type { ComposableSampler, SamplingIntent } from './types'; class ComposableAnnotatingSampler implements ComposableSampler { - private readonly attributes: Attributes; private readonly delegate: ComposableSampler; + private readonly attributes: Attributes; private readonly description: string; - constructor(attributes: Attributes, delegate: ComposableSampler) { + constructor(delegate: ComposableSampler, attributes: Attributes) { // Shallow copy `attributes` to avoid changes to the original object (at // least top-level fields) impacting the sampler. Freeze the object so // we can return it in `getSamplingIntent` without copying there. this.attributes = Object.freeze({ ...attributes }); this.delegate = delegate; - this.description = `ComposableAnnotatingSampler(attributes, delegate=${delegate})`; + this.description = `ComposableAnnotatingSampler(delegate=${delegate}, attributes)`; } getSamplingIntent( @@ -46,12 +46,14 @@ class ComposableAnnotatingSampler implements ComposableSampler { attributes, links ); - if (!intent.attributes) { - intent.attributes = this.attributes; - } else { - intent.attributes = { ...intent.attributes, ...this.attributes }; - } - return intent; + return { + threshold: intent.threshold, + thresholdReliable: intent.thresholdReliable, + attributes: intent.attributes + ? { ...intent.attributes, ...this.attributes } + : this.attributes, + updateTraceState: intent.updateTraceState, + }; } toString(): string { @@ -60,8 +62,8 @@ class ComposableAnnotatingSampler implements ComposableSampler { } export function createComposableAnnotatingSampler( - attributes: Attributes, - delegate: ComposableSampler + delegate: ComposableSampler, + attributes: Attributes ): ComposableSampler { - return new ComposableAnnotatingSampler(attributes, delegate); + return new ComposableAnnotatingSampler(delegate, attributes); } diff --git a/experimental/packages/sampler-composite/src/traceidratio.ts b/experimental/packages/sampler-composite/src/traceidratio.ts index 85f342c8c14..f40ce268b30 100644 --- a/experimental/packages/sampler-composite/src/traceidratio.ts +++ b/experimental/packages/sampler-composite/src/traceidratio.ts @@ -32,18 +32,18 @@ class ComposableTraceIDRatioBasedSampler implements ComposableSampler { const thresholdStr = threshold === MAX_THRESHOLD ? 'max' : serializeTh(threshold); if (threshold !== MAX_THRESHOLD) { - this.intent = { + this.intent = Object.freeze({ threshold: threshold, thresholdReliable: true, - }; + }); } else { // Same as AlwaysOff, notably the threshold is not considered reliable. The spec mentions // returning an instance of ComposableAlwaysOffSampler in this case but it seems clearer // if the description of the sampler matches the user's request. - this.intent = { + this.intent = Object.freeze({ threshold: INVALID_THRESHOLD, thresholdReliable: false, - }; + }); } this.description = `ComposableTraceIDRatioBasedSampler(threshold=${thresholdStr}, ratio=${ratio})`; } diff --git a/experimental/packages/sampler-composite/test/annotating.test.ts b/experimental/packages/sampler-composite/test/annotating.test.ts index 74e6192ef44..384e1b92c52 100644 --- a/experimental/packages/sampler-composite/test/annotating.test.ts +++ b/experimental/packages/sampler-composite/test/annotating.test.ts @@ -27,10 +27,10 @@ describe('ComposableAnnotatingSampler', () => { const alwaysOnSampler = createComposableAlwaysOnSampler(); it('should have a description', () => { - const sampler = createComposableAnnotatingSampler({}, alwaysOnSampler); + const sampler = createComposableAnnotatingSampler(alwaysOnSampler, {}); assert.strictEqual( sampler.toString(), - 'ComposableAnnotatingSampler(attributes, delegate=ComposableAlwaysOnSampler)' + 'ComposableAnnotatingSampler(delegate=ComposableAlwaysOnSampler, attributes)' ); }); @@ -44,8 +44,8 @@ describe('ComposableAnnotatingSampler', () => { [] ); const sampler = createComposableAnnotatingSampler( - { foo: 'bar' }, - alwaysOnSampler + alwaysOnSampler, + { foo: 'bar' } ); const intent = sampler.getSamplingIntent( context.active(), @@ -65,11 +65,11 @@ describe('ComposableAnnotatingSampler', () => { it('should merge attributes', () => { const sampler = createComposableAnnotatingSampler( - { foo: 'bar', spam: 'eggs' }, createComposableAnnotatingSampler( - { foo: 'baz', wuz: 'here' }, - alwaysOnSampler - ) + alwaysOnSampler, + { foo: 'baz', wuz: 'here' } + ), + { foo: 'bar', spam: 'eggs' }, ); const intent = sampler.getSamplingIntent( context.active(), From 4bbacf9bbb28b800f012858d3b010e82ec5ccbf1 Mon Sep 17 00:00:00 2001 From: Trent Mick Date: Thu, 15 Jan 2026 15:04:30 -0800 Subject: [PATCH 3/5] feat: add ComposableRuleBasedSampler --- .../sampler-composite/src/annotating.ts | 2 +- .../packages/sampler-composite/src/index.ts | 1 + .../sampler-composite/src/rulebased.ts | 71 ++++++++++ .../packages/sampler-composite/src/types.ts | 5 + .../sampler-composite/test/annotating.test.ts | 23 ++-- .../sampler-composite/test/rulebased.test.ts | 128 ++++++++++++++++++ 6 files changed, 217 insertions(+), 13 deletions(-) create mode 100644 experimental/packages/sampler-composite/src/rulebased.ts create mode 100644 experimental/packages/sampler-composite/test/rulebased.test.ts diff --git a/experimental/packages/sampler-composite/src/annotating.ts b/experimental/packages/sampler-composite/src/annotating.ts index 60cf5300ff8..9092626496c 100644 --- a/experimental/packages/sampler-composite/src/annotating.ts +++ b/experimental/packages/sampler-composite/src/annotating.ts @@ -27,7 +27,7 @@ class ComposableAnnotatingSampler implements ComposableSampler { // we can return it in `getSamplingIntent` without copying there. this.attributes = Object.freeze({ ...attributes }); this.delegate = delegate; - this.description = `ComposableAnnotatingSampler(delegate=${delegate}, attributes)`; + this.description = `ComposableAnnotatingSampler(${delegate}, attributes)`; } getSamplingIntent( diff --git a/experimental/packages/sampler-composite/src/index.ts b/experimental/packages/sampler-composite/src/index.ts index d36e0adfa46..8462c6210be 100644 --- a/experimental/packages/sampler-composite/src/index.ts +++ b/experimental/packages/sampler-composite/src/index.ts @@ -19,5 +19,6 @@ export { createComposableAlwaysOnSampler } from './alwayson'; export { createComposableTraceIDRatioBasedSampler } from './traceidratio'; export { createComposableParentThresholdSampler } from './parentthreshold'; export { createComposableAnnotatingSampler } from './annotating'; +export { createComposableRuleBasedSampler } from './rulebased'; export { createCompositeSampler } from './composite'; export type { ComposableSampler, SamplingIntent } from './types'; diff --git a/experimental/packages/sampler-composite/src/rulebased.ts b/experimental/packages/sampler-composite/src/rulebased.ts new file mode 100644 index 00000000000..559f5e46219 --- /dev/null +++ b/experimental/packages/sampler-composite/src/rulebased.ts @@ -0,0 +1,71 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Attributes, Context, Link, SpanKind } from '@opentelemetry/api'; + +import type { ComposableSampler, SamplingIntent, SamplingRule } from './types'; +import { INVALID_THRESHOLD } from './util'; + +const NON_SAMPLING_INTENT: SamplingIntent = Object.freeze({ + threshold: INVALID_THRESHOLD, + thresholdReliable: false, +}); + +class ComposableRuleBasedSampler implements ComposableSampler { + private readonly rules: SamplingRule[]; + private readonly description: string; + + constructor(rules: SamplingRule[]) { + this.rules = rules; + const ruleReprs = rules.map( + rule => `(${rule[0].name || '(anonymous)'}, ${rule[1].toString()})` + ); + this.description = `ComposableRuleBasedSampler([${ruleReprs.join(', ')}])`; + } + + getSamplingIntent( + context: Context, + traceId: string, + spanName: string, + spanKind: SpanKind, + attributes: Attributes, + links: Link[] + ): SamplingIntent { + for (const [predicate, sampler] of this.rules) { + if (predicate(context, traceId, spanName, spanKind, attributes, links)) { + return sampler.getSamplingIntent( + context, + traceId, + spanName, + spanKind, + attributes, + links + ); + } + } + return NON_SAMPLING_INTENT; + } + + toString(): string { + return this.description; + } +} + +export function createComposableRuleBasedSampler( + rules: SamplingRule[] +): ComposableSampler { + return new ComposableRuleBasedSampler(rules); +} diff --git a/experimental/packages/sampler-composite/src/types.ts b/experimental/packages/sampler-composite/src/types.ts index 3743a38c071..e4f08d293ce 100644 --- a/experimental/packages/sampler-composite/src/types.ts +++ b/experimental/packages/sampler-composite/src/types.ts @@ -42,3 +42,8 @@ export interface ComposableSampler { /** Returns the sampler name or short description with the configuration. */ toString(): string; } + +export type SamplingPredicate = ( + ...args: Parameters +) => boolean; +export type SamplingRule = [SamplingPredicate, ComposableSampler]; diff --git a/experimental/packages/sampler-composite/test/annotating.test.ts b/experimental/packages/sampler-composite/test/annotating.test.ts index 384e1b92c52..91a88dfc631 100644 --- a/experimental/packages/sampler-composite/test/annotating.test.ts +++ b/experimental/packages/sampler-composite/test/annotating.test.ts @@ -30,7 +30,7 @@ describe('ComposableAnnotatingSampler', () => { const sampler = createComposableAnnotatingSampler(alwaysOnSampler, {}); assert.strictEqual( sampler.toString(), - 'ComposableAnnotatingSampler(delegate=ComposableAlwaysOnSampler, attributes)' + 'ComposableAnnotatingSampler(ComposableAlwaysOnSampler, attributes)' ); }); @@ -43,10 +43,9 @@ describe('ComposableAnnotatingSampler', () => { {}, [] ); - const sampler = createComposableAnnotatingSampler( - alwaysOnSampler, - { foo: 'bar' } - ); + const sampler = createComposableAnnotatingSampler(alwaysOnSampler, { + foo: 'bar', + }); const intent = sampler.getSamplingIntent( context.active(), 'unused', @@ -65,11 +64,11 @@ describe('ComposableAnnotatingSampler', () => { it('should merge attributes', () => { const sampler = createComposableAnnotatingSampler( - createComposableAnnotatingSampler( - alwaysOnSampler, - { foo: 'baz', wuz: 'here' } - ), - { foo: 'bar', spam: 'eggs' }, + createComposableAnnotatingSampler(alwaysOnSampler, { + foo: 'baz', + wuz: 'here', + }), + { foo: 'bar', spam: 'eggs' } ); const intent = sampler.getSamplingIntent( context.active(), @@ -80,9 +79,9 @@ describe('ComposableAnnotatingSampler', () => { [] ); assert.deepStrictEqual(intent.attributes, { - foo: 'bar', // outer annotating sampler wins + foo: 'bar', // outer annotating sampler wins spam: 'eggs', - wuz: 'here' + wuz: 'here', }); }); }); diff --git a/experimental/packages/sampler-composite/test/rulebased.test.ts b/experimental/packages/sampler-composite/test/rulebased.test.ts new file mode 100644 index 00000000000..99fe5c3649f --- /dev/null +++ b/experimental/packages/sampler-composite/test/rulebased.test.ts @@ -0,0 +1,128 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as assert from 'assert'; + +import type { Attributes, Context } from '@opentelemetry/api'; +import { context, SpanKind } from '@opentelemetry/api'; + +import { + createComposableAlwaysOffSampler, + createComposableAlwaysOnSampler, + createComposableAnnotatingSampler, + createComposableRuleBasedSampler, +} from '../src'; +import type { SamplingPredicate } from '../src/types'; +import { INVALID_THRESHOLD } from '../src/util'; + +describe('ComposableRuleBasedSampler', () => { + // Building a test sampler something like the example given in the spec: + // https://opentelemetry.io/docs/specs/otel/trace/sdk/#composablerulebased + const alwaysOn = createComposableAlwaysOnSampler(); + const alwaysOff = createComposableAlwaysOffSampler(); + const isHealthCheck: SamplingPredicate = ( + _context, + _traceId, + _spanName, + _spanKind, + attributes: Attributes, + _links + ) => { + return attributes['http.route'] === '/healthcheck'; + }; + function isCheckout( + _context: Context, + _traceId: string, + _spanName: string, + _spanKind: SpanKind, + attributes: Attributes + ) { + return attributes['http.route'] === '/checkout'; + } + const sampler = createComposableRuleBasedSampler([ + [isHealthCheck, alwaysOff], + [ + isCheckout, + createComposableAnnotatingSampler(alwaysOn, { is: 'checkout' }), + ], + [ + () => true, + createComposableAnnotatingSampler(alwaysOn, { is: 'catchall' }), + ], + ]); + + it('should have a description', () => { + assert.strictEqual( + sampler.toString(), + 'ComposableRuleBasedSampler([' + + '(isHealthCheck, ComposableAlwaysOffSampler), ' + + '(isCheckout, ComposableAnnotatingSampler(ComposableAlwaysOnSampler, attributes)), ' + + '((anonymous), ComposableAnnotatingSampler(ComposableAlwaysOnSampler, attributes))' + + '])' + ); + }); + + it('should use the matched samplers', () => { + let intent; + + intent = sampler.getSamplingIntent( + context.active(), + 'unused-trace-id', + 'span-name', + SpanKind.SERVER, + { 'http.route': '/healthcheck' }, + [] + ); + assert.strictEqual(intent.threshold, -1n); // used the alwaysOff sampler + assert.strictEqual(intent.attributes, undefined); + + intent = sampler.getSamplingIntent( + context.active(), + 'unused-trace-id', + 'span-name', + SpanKind.SERVER, + { 'http.route': '/checkout' }, + [] + ); + assert.strictEqual(intent.threshold, 0n); // used the alwaysOn sampler + assert.deepStrictEqual(intent.attributes, { is: 'checkout' }); + + intent = sampler.getSamplingIntent( + context.active(), + 'unused-trace-id', + 'span-name', + SpanKind.SERVER, + { 'http.route': '/another' }, + [] + ); + assert.strictEqual(intent.threshold, 0n); // used the alwaysOn sampler + assert.deepStrictEqual(intent.attributes, { is: 'catchall' }); + }); + + it('should fallback to a non-sampling intent', () => { + const sampler2 = createComposableRuleBasedSampler([]); + const intent = sampler2.getSamplingIntent( + context.active(), + 'unused-trace-id', + 'span-name', + SpanKind.SERVER, + {}, + [] + ); + assert.strictEqual(intent.threshold, INVALID_THRESHOLD); + assert.strictEqual(intent.thresholdReliable, false); + }); +}); From cd13e5b542ae576de38f23dc656ab2d3aedaa394 Mon Sep 17 00:00:00 2001 From: Trent Mick Date: Thu, 15 Jan 2026 15:25:56 -0800 Subject: [PATCH 4/5] changelog entry --- experimental/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/experimental/CHANGELOG.md b/experimental/CHANGELOG.md index 76c37928741..503f7dc0cb1 100644 --- a/experimental/CHANGELOG.md +++ b/experimental/CHANGELOG.md @@ -10,6 +10,8 @@ For notes on migrating to 2.x / 0.200.x see [the upgrade guide](doc/upgrade-to-2 ### :rocket: Features +* feat(sampler-composite): add ComposableAnnotatingSampler and ComposableRuleBasedSampler [#6305](https://github.com/open-telemetry/opentelemetry-js/pull/6305) @trentm + ### :bug: Bug Fixes ### :books: Documentation From ce2b0629e7519fa314647c9a7ff4d459bb2c27cb Mon Sep 17 00:00:00 2001 From: Trent Mick Date: Fri, 6 Feb 2026 11:00:27 -0800 Subject: [PATCH 5/5] fix placement of this changelog entry, it was *not* in the 0.211.0 release --- experimental/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/experimental/CHANGELOG.md b/experimental/CHANGELOG.md index a63c6bf364c..a6b668203a9 100644 --- a/experimental/CHANGELOG.md +++ b/experimental/CHANGELOG.md @@ -17,6 +17,7 @@ For notes on migrating to 2.x / 0.200.x see [the upgrade guide](doc/upgrade-to-2 * feat(sampler-composite): add ComposableAnnotatingSampler and ComposableRuleBasedSampler [#6305](https://github.com/open-telemetry/opentelemetry-js/pull/6305) @trentm * feat(configuration): parse config for rc 3 [#6304](https://github.com/open-telemetry/opentelemetry-js/pull/6304) @maryliag +* feat(instrumentation): use the `internals: true` option with import-in-the-middle hook, allowing instrumentations to hook internal files in ES modules [#6344](https://github.com/open-telemetry/opentelemetry-js/pull/6344) @trentm ### :bug: Bug Fixes @@ -39,7 +40,6 @@ For notes on migrating to 2.x / 0.200.x see [the upgrade guide](doc/upgrade-to-2 ### :rocket: Features -* feat(instrumentation): use the `internals: true` option with import-in-the-middle hook, allowing instrumentations to hook internal files in ES modules [#6344](https://github.com/open-telemetry/opentelemetry-js/pull/6344) @trentm * feat(sdk-logs): export event name from ConsoleLogRecordExporter [#6310](https://github.com/open-telemetry/opentelemetry-js/pull/6310) @aicest ### :bug: Bug Fixes