Skip to content

Commit 9a8b12d

Browse files
fix: flag evaluation span name (#117)
Flag evaluation requires specific name of span in grouping aggregation. Change span name to `evaluation` from methodName of flag. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Implements spec-aligned tracing and minor config/UI updates. > > - Observability: `ObservabilityHook` now creates spans named `evaluation` with `feature_flag.key`, `feature_flag.provider.name`, and `feature_flag.context.id` attributes; feature flag evaluation event includes variation index, optional result value, and `reason.inExperiment`; span is retrieved from `seriesData`, event added, and span ended. Defaults `withSpans`/`withValue` to `true`. > - Session Replay: `InitializeSessionOperation` sets `manualStart` to `false` and uses `sdkVersion` for `clientVersion`/`firstloadVersion`; imports `LaunchDarklyObservability`. > - Test App: Adjusts button labels/layout; adds "Span & Flag Eval" action alongside a dedicated "Network Request" button with progress/disabled state. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 548df4c. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent f77fc79 commit 9a8b12d

File tree

3 files changed

+79
-73
lines changed

3 files changed

+79
-73
lines changed
Lines changed: 53 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Foundation
22
import LaunchDarkly
33
#if !LD_COCOAPODS
4-
import Common
4+
import Common
55
#endif
66

77
final class ObservabilityHook: Hook {
@@ -13,8 +13,8 @@ final class ObservabilityHook: Hook {
1313
private let options: Options
1414

1515
init(plugin: Observability,
16-
withSpans: Bool,
17-
withValue: Bool,
16+
withSpans: Bool = true,
17+
withValue: Bool = true,
1818
version: String,
1919
options: Options) {
2020
self.plugin = plugin
@@ -32,60 +32,63 @@ final class ObservabilityHook: Hook {
3232
seriesContext: EvaluationSeriesContext,
3333
seriesData: EvaluationSeriesData
3434
) -> EvaluationSeriesData {
35-
// queue.sync {
36-
guard withSpans else { return seriesData }
37-
38-
/// Requirement 1.2.3.6
39-
/// https://github.com/launchdarkly/sdk-specs/tree/main/specs/OTEL-openteletry-integration#requirement-1236
40-
let span = LDObserve.shared.startSpan(
41-
name: "LDClient.\(seriesContext.methodName)",
42-
attributes: options.resourceAttributes
43-
)
44-
45-
var mutableSeriesData = seriesData
46-
mutableSeriesData[Self.DATA_KEY_SPAN] = span
47-
48-
return mutableSeriesData
49-
// }
35+
guard withSpans else { return seriesData }
36+
37+
/// Requirement 1.2.3.6
38+
/// https://github.com/launchdarkly/sdk-specs/tree/main/specs/OTEL-openteletry-integration#requirement-1236
39+
var resourceAttributes = options.resourceAttributes
40+
resourceAttributes[Self.SEMCONV_FEATURE_FLAG_KEY] = .string(seriesContext.flagKey)
41+
resourceAttributes[Self.SEMCONV_FEATURE_FLAG_PROVIDER_NAME] = .string(Self.PROVIDER_NAME)
42+
resourceAttributes[Self.SEMCONV_FEATURE_FLAG_CONTEXT_ID] = .string(seriesContext.context.fullyQualifiedKey())
43+
44+
let span = LDObserve.shared.startSpan(
45+
name: Self.FEATURE_FLAG_SPAN_NAME,
46+
attributes: resourceAttributes
47+
)
48+
49+
var mutableSeriesData = seriesData
50+
mutableSeriesData[Self.DATA_KEY_SPAN] = span
51+
return mutableSeriesData
5052
}
5153

5254
public func afterEvaluation(
5355
seriesContext: EvaluationSeriesContext,
5456
seriesData: EvaluationSeriesData,
5557
evaluationDetail: LDEvaluationDetail<LDValue>
5658
) -> EvaluationSeriesData {
57-
// queue.sync {
58-
/// Requirement 1.2.2.2
59-
/// The feature_flag event MUST have the following attributes: feature_flag.key, feature_flag.provider.name, and feature_flag.context.id.
60-
var resourceAttributes = [String: AttributeValue]()
61-
resourceAttributes[Self.SEMCONV_FEATURE_FLAG_KEY] = .string(seriesContext.flagKey)
62-
resourceAttributes[Self.SEMCONV_FEATURE_FLAG_PROVIDER_NAME] = .string(Self.PROVIDER_NAME)
63-
resourceAttributes[Self.SEMCONV_FEATURE_FLAG_CONTEXT_ID] = .string(seriesContext.context.fullyQualifiedKey())
64-
65-
if let lDValue = evaluationDetail.reason?[Self.CUSTOM_FEATURE_FLAG_RESULT_REASON_IN_EXPERIMENT] {
66-
if case let .bool(inExperiment) = lDValue {
67-
resourceAttributes[Self.CUSTOM_FEATURE_FLAG_RESULT_REASON_IN_EXPERIMENT] = .bool(inExperiment)
68-
}
69-
}
70-
71-
if withValue {
72-
if let stringified = JSON.stringify(evaluationDetail.value) {
73-
resourceAttributes[Self.SEMCONV_FEATURE_FLAG_RESULT_VALUE] = .string(stringified) // .string is from Otel AttributeValue
74-
}
75-
}
76-
77-
if let index = evaluationDetail.variationIndex {
78-
resourceAttributes[Self.CUSTOM_FEATURE_FLAG_RESULT_VARIATION_INDEX] = .double(Double(index))
59+
/// Requirement 1.2.2.2
60+
/// The feature_flag event MUST have the following attributes: feature_flag.key, feature_flag.provider.name, and feature_flag.context.id.
61+
guard let span = seriesData[Self.DATA_KEY_SPAN] as? Span else {
62+
return seriesData
63+
}
64+
65+
/// Requirement 1.2.3.6
66+
/// https://github.com/launchdarkly/sdk-specs/tree/main/specs/OTEL-openteletry-integration#requirement-1236
67+
var eventAttributes = options.resourceAttributes
68+
eventAttributes[Self.SEMCONV_FEATURE_FLAG_KEY] = .string(seriesContext.flagKey)
69+
eventAttributes[Self.SEMCONV_FEATURE_FLAG_PROVIDER_NAME] = .string(Self.PROVIDER_NAME)
70+
eventAttributes[Self.SEMCONV_FEATURE_FLAG_CONTEXT_ID] = .string(seriesContext.context.fullyQualifiedKey())
71+
72+
if let lDValue = evaluationDetail.reason?[Self.CUSTOM_FEATURE_FLAG_RESULT_REASON_IN_EXPERIMENT] {
73+
if case let .bool(inExperiment) = lDValue {
74+
eventAttributes[Self.CUSTOM_FEATURE_FLAG_RESULT_REASON_IN_EXPERIMENT] = .bool(inExperiment)
7975
}
80-
81-
let value = seriesData[Self.DATA_KEY_SPAN]
82-
if let span = value as? Span {
83-
span.addEvent(name: Self.EVENT_NAME, attributes: resourceAttributes, timestamp: Date())
84-
span.end()
76+
}
77+
78+
if withValue {
79+
if let stringified = JSON.stringify(evaluationDetail.value) {
80+
eventAttributes[Self.SEMCONV_FEATURE_FLAG_RESULT_VALUE] = .string(stringified) // .string is from Otel AttributeValue
8581
}
86-
87-
return seriesData
88-
// }
82+
}
83+
84+
if let index = evaluationDetail.variationIndex {
85+
eventAttributes[Self.CUSTOM_FEATURE_FLAG_RESULT_VARIATION_INDEX] = .double(Double(index))
86+
}
87+
88+
span.addEvent(name: Self.EVENT_NAME, attributes: eventAttributes, timestamp: Date())
89+
90+
span.end()
91+
return seriesData
8992
}
9093

9194
public func afterIdentify(seriesContext: IdentifySeriesContext, seriesData: IdentifySeriesData, result: IdentifyResult) -> IdentifySeriesData {
@@ -126,7 +129,8 @@ extension ObservabilityHook {
126129
static let SEMCONV_FEATURE_FLAG_RESULT_VALUE: String = "feature_flag.result.value"
127130
static let CUSTOM_FEATURE_FLAG_RESULT_VARIATION_INDEX: String = "feature_flag.result.variationIndex"
128131
static let CUSTOM_FEATURE_FLAG_RESULT_REASON_IN_EXPERIMENT: String = "feature_flag.result.reason.inExperiment"
129-
static let FEATURE_FLAG_SPAN_NAME = "evaluation" /// FEATURE_FLAG_SPAN_NAME
132+
static let FEATURE_FLAG_SET_ID = "feature_flag.set.id"
133+
static let FEATURE_FLAG_SPAN_NAME = "evaluation"
130134
static let FEATURE_FLAG_CONTEXT_ATTR = "feature_flag.contextKeys"
131135
static let IDENTIFY_RESULT_STATUS = "identify.result.status"
132136
}

Sources/LaunchDarklySessionReplay/Operations/InitializeSessionOperation.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Foundation
2+
import LaunchDarklyObservability
23

34
struct InitializeSessionVariables: Codable {
45
struct ClientConfig: Codable {
@@ -86,7 +87,7 @@ extension SessionReplayAPIService {
8687
privacySetting: "none",
8788
serviceName: context.serviceName,
8889
backendUrl: context.backendUrl,
89-
manualStart: true,
90+
manualStart: false,
9091
organizationID: context.sdkKey,
9192
environment: "production",
9293
sessionSecureID: sessionSecureId
@@ -192,8 +193,8 @@ extension SessionReplayAPIService {
192193
enableStrictPrivacy: false,
193194
privacySetting: "none",
194195
enableRecordingNetworkContents: false,
195-
clientVersion: "9.18.23",
196-
firstloadVersion: "9.18.23",
196+
clientVersion: sdkVersion,
197+
firstloadVersion: sdkVersion,
197198
clientConfig: clientConfigString,
198199
environment: "production",
199200
id: "31MMpqmDG2DsZvbxo0Lzx4xelbt7",

TestApp/Sources/MainMenuView.swift

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -90,44 +90,45 @@ struct MainMenuView: View {
9090
}
9191

9292
HStack {
93-
Button {
94-
viewModel.recordSpanAndVariation()
95-
} label: {
96-
Text("span")
97-
}
98-
.buttonStyle(.borderedProminent)
99-
10093
Button {
10194
viewModel.recordLogs()
10295
} label: {
103-
Text("logs")
96+
Text("Logs")
10497
}
10598
.buttonStyle(.borderedProminent)
10699

107100
Button {
108101
viewModel.recordCounterMetric()
109102
} label: {
110-
Text("metric: counter")
103+
Text("Metric: counter")
111104
}
112105
.buttonStyle(.borderedProminent)
113106
}
114107

115-
Button {
116-
Task {
117-
await viewModel.performNetworkRequest()
108+
HStack {
109+
Button {
110+
viewModel.recordSpanAndVariation()
111+
} label: {
112+
Text("Span & Flag Eval")
118113
}
119-
} label: {
120-
if viewModel.isNetworkInProgress {
121-
ProgressView {
122-
Text("get request to launchdarkly.com...")
114+
.buttonStyle(.borderedProminent)
115+
Button {
116+
Task {
117+
await viewModel.performNetworkRequest()
118+
}
119+
} label: {
120+
if viewModel.isNetworkInProgress {
121+
ProgressView {
122+
Text("get request to launchdarkly.com...")
123+
}
124+
} else {
125+
Text("Network Request")
123126
}
124-
} else {
125-
Text("network request: span")
126127
}
128+
.buttonStyle(.borderedProminent)
129+
.disabled(viewModel.isNetworkInProgress)
127130
}
128-
.buttonStyle(.borderedProminent)
129-
.disabled(viewModel.isNetworkInProgress)
130-
131+
131132
HStack {
132133
Button {
133134
viewModel.recordError()

0 commit comments

Comments
 (0)