Skip to content

Commit 3dbcfc5

Browse files
niieanibbrzoska
authored andcommitted
feat: implement promoteSpanAttributes feature
1 parent b033233 commit 3dbcfc5

6 files changed

+515
-74
lines changed

src/v3/Trace.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1189,6 +1189,7 @@ export class Trace<
11891189
variants: definition.variants,
11901190
labelMatching: definition.labelMatching,
11911191
debounceWindow: definition.debounceWindow,
1192+
promoteSpanAttributes: definition.promoteSpanAttributes,
11921193

11931194
// below props are potentially mutable elements of the definition, let's make local copies:
11941195
requiredSpans: [...definition.requiredSpans],

src/v3/ensureMatcherFn.ts

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,35 @@
1-
import { fromDefinition, type SpanMatch, type SpanMatcherFn } from './matchSpan'
1+
import {
2+
fromDefinition,
3+
type SpanMatch,
4+
type SpanMatcherFn,
5+
type SpanMatcherTags,
6+
} from './matchSpan'
27
import type { LabelMatchingFnsRecord, LabelMatchingInputRecord } from './types'
38
import type { ArrayWithAtLeastOneElement } from './typeUtils'
49

10+
export function applyDefaultTags<
11+
const SelectedRelationNameT extends keyof RelationSchemasT,
12+
const RelationSchemasT,
13+
const VariantsT extends string,
14+
>(
15+
matcherFn: SpanMatcherFn<SelectedRelationNameT, RelationSchemasT, VariantsT>,
16+
defaultTags?: SpanMatcherTags,
17+
): SpanMatcherFn<SelectedRelationNameT, RelationSchemasT, VariantsT> {
18+
if (!defaultTags) {
19+
return matcherFn
20+
}
21+
for (const key of Object.keys(defaultTags)) {
22+
const k = key as keyof SpanMatcherTags
23+
// only add default tags to matcher if they are not already defined
24+
if (!matcherFn[k]) {
25+
// @ts-expect-error hard to type
26+
// eslint-disable-next-line no-param-reassign
27+
matcherFn[k] = defaultTags[k]
28+
}
29+
}
30+
return matcherFn
31+
}
32+
533
export function ensureMatcherFn<
634
const SelectedRelationNameT extends keyof RelationSchemasT,
735
const RelationSchemasT,
@@ -13,12 +41,16 @@ export function ensureMatcherFn<
1341
> = SpanMatch<SelectedRelationNameT, RelationSchemasT, VariantsT>,
1442
>(
1543
matcherFnOrDefinition: MatcherInputT,
44+
defaultTags?: SpanMatcherTags,
1645
): SpanMatcherFn<SelectedRelationNameT, RelationSchemasT, VariantsT> {
17-
return typeof matcherFnOrDefinition === 'function'
18-
? matcherFnOrDefinition
19-
: fromDefinition<SelectedRelationNameT, RelationSchemasT, VariantsT>(
20-
matcherFnOrDefinition,
21-
)
46+
return applyDefaultTags(
47+
typeof matcherFnOrDefinition === 'function'
48+
? matcherFnOrDefinition
49+
: fromDefinition<SelectedRelationNameT, RelationSchemasT, VariantsT>(
50+
matcherFnOrDefinition,
51+
),
52+
defaultTags,
53+
)
2254
}
2355

2456
function arrayHasAtLeastOneElement<T>(

src/v3/recordingComputeUtils.test.ts

Lines changed: 178 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,11 @@
11
import { describe, expect, it, vitest as jest } from 'vitest'
22
import { getSpanSummaryAttributes } from './convertToRum'
3-
import {
4-
fromDefinition,
5-
type SpanMatcherFn,
6-
withMatchingRelations,
7-
} from './matchSpan'
3+
import { fromDefinition } from './matchSpan'
84
import {
95
createTraceRecording,
106
getComputedSpans,
117
getComputedValues,
128
} from './recordingComputeUtils'
13-
import type { SpanAndAnnotation } from './spanAnnotationTypes'
14-
import type { Span } from './spanTypes'
159
import {
1610
createMockSpanAndAnnotation,
1711
createTimestamp,
@@ -752,4 +746,181 @@ describe('recordingComputeUtils', () => {
752746
expect(recording.variant).toBe('origin')
753747
})
754748
})
749+
750+
describe('promoteSpanAttributesForTrace and attribute promotion', () => {
751+
const promotedAttributesTraceDefinition = {
752+
...baseDefinitionFixture,
753+
promoteSpanAttributes: [
754+
{
755+
span: { name: 'foo-span' },
756+
attributes: ['foo', 'bar'],
757+
},
758+
{
759+
span: { name: 'baz-span' },
760+
attributes: ['baz'],
761+
},
762+
],
763+
}
764+
765+
it('should promote specified attributes from last matching spans to trace', () => {
766+
const recording = createTraceRecording(
767+
{
768+
definition: promotedAttributesTraceDefinition,
769+
recordedItems: new Set([
770+
createMockSpanAndAnnotation(100, {
771+
name: 'foo-span',
772+
attributes: { foo: 1, bar: 2, unused: 42 },
773+
}),
774+
createMockSpanAndAnnotation(200, {
775+
name: 'foo-span',
776+
attributes: { foo: 7, bar: 8 },
777+
}),
778+
createMockSpanAndAnnotation(300, {
779+
name: 'baz-span',
780+
attributes: { baz: 'hello' },
781+
}),
782+
]),
783+
input: {
784+
id: 'test',
785+
startTime: createTimestamp(0),
786+
relatedTo: {},
787+
variant: 'origin',
788+
},
789+
recordedItemsByLabel: {},
790+
},
791+
{
792+
transitionFromState: 'active',
793+
transitionToState: 'complete',
794+
lastRelevantSpanAndAnnotation: undefined,
795+
completeSpanAndAnnotation: undefined,
796+
cpuIdleSpanAndAnnotation: undefined,
797+
lastRequiredSpanAndAnnotation: undefined,
798+
},
799+
)
800+
// Should select last foo-span (timestamp 200) for foo/bar, and baz-span for baz
801+
expect(recording.attributes.foo).toBe(7)
802+
expect(recording.attributes.bar).toBe(8)
803+
expect(recording.attributes.baz).toBe('hello')
804+
expect('unused' in recording.attributes).toBe(false)
805+
})
806+
807+
it('should not set unset promoted attributes if not found', () => {
808+
const partialAttrDefinition = {
809+
...baseDefinitionFixture,
810+
promoteSpanAttributes: [
811+
{
812+
span: { name: 'foo-span' },
813+
attributes: ['foo', 'bar'],
814+
},
815+
{
816+
span: { name: 'no-match' },
817+
attributes: ['baz', 'shouldNotBeSet'],
818+
},
819+
],
820+
}
821+
const recording = createTraceRecording(
822+
{
823+
definition: partialAttrDefinition,
824+
recordedItems: new Set([
825+
createMockSpanAndAnnotation(111, {
826+
name: 'foo-span',
827+
attributes: { foo: 99 },
828+
}),
829+
]),
830+
input: {
831+
id: 'test',
832+
startTime: createTimestamp(0),
833+
relatedTo: {},
834+
variant: 'origin',
835+
},
836+
recordedItemsByLabel: {},
837+
},
838+
{
839+
transitionFromState: 'active',
840+
transitionToState: 'complete',
841+
lastRelevantSpanAndAnnotation: undefined,
842+
completeSpanAndAnnotation: undefined,
843+
cpuIdleSpanAndAnnotation: undefined,
844+
lastRequiredSpanAndAnnotation: undefined,
845+
},
846+
)
847+
expect(recording.attributes.foo).toBe(99)
848+
expect('bar' in recording.attributes).toBe(false)
849+
expect('baz' in recording.attributes).toBe(false)
850+
expect('shouldNotBeSet' in recording.attributes).toBe(false)
851+
})
852+
853+
it('should allow attribute promotion on interruption', () => {
854+
const recording = createTraceRecording(
855+
{
856+
definition: promotedAttributesTraceDefinition,
857+
recordedItems: new Set([
858+
createMockSpanAndAnnotation(100, {
859+
name: 'foo-span',
860+
attributes: { foo: 'z' },
861+
}),
862+
createMockSpanAndAnnotation(200, {
863+
name: 'baz-span',
864+
attributes: { baz: 10 },
865+
}),
866+
]),
867+
input: {
868+
id: 'test',
869+
startTime: createTimestamp(0),
870+
relatedTo: {},
871+
variant: 'origin',
872+
},
873+
recordedItemsByLabel: {},
874+
},
875+
{
876+
transitionFromState: 'active',
877+
transitionToState: 'interrupted',
878+
interruptionReason: 'timeout',
879+
lastRelevantSpanAndAnnotation: undefined,
880+
},
881+
)
882+
expect(recording.attributes.foo).toBe('z')
883+
expect(recording.attributes.baz).toBe(10)
884+
})
885+
886+
it('should allow original trace attributes to take precedence over promoted', () => {
887+
const definition = {
888+
...promotedAttributesTraceDefinition,
889+
}
890+
891+
const recording = createTraceRecording(
892+
{
893+
definition,
894+
recordedItems: new Set([
895+
createMockSpanAndAnnotation(100, {
896+
name: 'foo-span',
897+
attributes: { foo: 'notUsed' },
898+
}),
899+
createMockSpanAndAnnotation(200, {
900+
name: 'baz-span',
901+
attributes: { baz: 111 },
902+
}),
903+
]),
904+
input: {
905+
id: 'test',
906+
startTime: createTimestamp(0),
907+
relatedTo: {},
908+
variant: 'origin',
909+
attributes: { foo: 'owned', baz: 'shouldWin' },
910+
},
911+
recordedItemsByLabel: {},
912+
},
913+
{
914+
transitionFromState: 'active',
915+
transitionToState: 'complete',
916+
lastRelevantSpanAndAnnotation: undefined,
917+
completeSpanAndAnnotation: undefined,
918+
cpuIdleSpanAndAnnotation: undefined,
919+
lastRequiredSpanAndAnnotation: undefined,
920+
},
921+
)
922+
expect(recording.attributes.foo).toBe('owned')
923+
expect(recording.attributes.baz).toBe('shouldWin')
924+
})
925+
})
755926
})

0 commit comments

Comments
 (0)