Skip to content

Commit 56bd0b9

Browse files
authored
DEMRUM-1307: Implement local sampling (#305)
* feat: Implement session (agent) sampling mechanism and related logic * chore: Self review * chore: Add sampling configuration to DevelApp * fix: Fix various compilation errors in tests * fix: Properly fix Agent tests, introduce .shared singleton resetting helper method * fix: Fix SplunkCrashReportsTests crashreporter dependency
1 parent ce72585 commit 56bd0b9

File tree

21 files changed

+743
-19
lines changed

21 files changed

+743
-19
lines changed

Applications/DevelApp/DevelApp/DevelAppApp.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ struct DevelAppApp: App {
2828
deploymentEnvironment: "dev"
2929
)
3030
.enableDebugLogging(true)
31+
// Sampled-out agent
32+
//.sessionSamplingRate(0)
33+
.sessionSamplingRate(1)
3134

3235
let agent = try! SplunkRum.install(with: agentConfig)
3336

Package.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,11 @@ func generateMainTargets() -> [Target] {
177177
),
178178
.testTarget(
179179
name: "SplunkCrashReportsTests",
180-
dependencies: ["SplunkCrashReports", "SplunkCommon", "PLCrashReporter"],
180+
dependencies: [
181+
"SplunkCrashReports",
182+
"SplunkCommon",
183+
.product(name: "CrashReporter", package: "PLCrashReporter")
184+
],
181185
path: "SplunkCrashReports/Tests"
182186
),
183187

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
//
2+
/*
3+
Copyright 2025 Splunk Inc.
4+
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
*/
17+
18+
import Foundation
19+
20+
/// A protocol designed for objects that can provide random numbers.
21+
protocol RandomNumberProvider {
22+
23+
/// Generates a random `Double` within the specified inclusive range.
24+
///
25+
/// - Parameters:
26+
/// - range: A `ClosedRange<Double>` specifying the lower and upper bounds for the random number.
27+
/// - Returns: A random `Double` within the specified range.
28+
func randomNumber(in range: ClosedRange<Double>) -> Double
29+
}
30+
31+
/// A default implementation of `RandomNumberProvider` that utilizes the
32+
/// system's `Double.random(in:)` function for generating random numbers.
33+
///
34+
/// This provider is suitable for most general-purpose statistical samplers.
35+
struct SystemRandomNumberProvider: RandomNumberProvider {
36+
37+
/// Generates a random `Double` using `Double.random(in: range)`.
38+
///
39+
/// - Parameter range: The inclusive range within which to generate the random number.
40+
/// - Returns: A random `Double` within the specified range.
41+
func randomNumber(in range: ClosedRange<Double>) -> Double {
42+
return Double.random(in: range)
43+
}
44+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
//
2+
/*
3+
Copyright 2025 Splunk Inc.
4+
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
*/
17+
18+
/// A protocol defining the fundamental behaviour for all samplers.
19+
protocol BaseSampler: AnyObject {
20+
21+
/// Calculates a sampling decision.
22+
///
23+
/// This function determines whether an item (or session) should be recorded or sampled out
24+
/// based on the sampler's specific logic and configuration.
25+
///
26+
/// - Returns: A `SamplingDecision` indicating whether to sample in (`.notSampledOut`)
27+
/// or sample out (`.sampledOut`).
28+
func sample() -> SamplingDecision
29+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
//
2+
/*
3+
Copyright 2025 Splunk Inc.
4+
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
*/
17+
18+
/// An internal helper implementation of `AgentSessionSampler` that always returns `.sampledOut`.
19+
final class AlwaysOffAgentSampler: AgentSessionSampler {
20+
21+
// MARK: - Constants
22+
23+
let upperBound: Double = 0
24+
25+
let lowerBound: Double = 0
26+
27+
let probability: Double = 0
28+
29+
30+
// MARK: - Configuration
31+
32+
/// A non-operational configuration method.
33+
///
34+
/// - Parameter configuration: The agent configuration (ignored).
35+
func configure(with configuration: any AgentConfigurationProtocol) {}
36+
37+
38+
// MARK: - Sampling
39+
40+
/// A non-operational sampling method.
41+
///
42+
/// - Returns: Always returns `.sampledOut`.
43+
func sample() -> SamplingDecision {
44+
return .sampledOut
45+
}
46+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
//
2+
/*
3+
Copyright 2025 Splunk Inc.
4+
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
*/
17+
18+
/// An internal helper implementation of `AgentSessionSampler` that always returns `.notSampledOut`.
19+
final class AlwaysOnAgentSampler: AgentSessionSampler {
20+
21+
// MARK: - Constants
22+
23+
let probability: Double = 1.0
24+
25+
let upperBound: Double = 1.0
26+
27+
let lowerBound: Double = 0.0
28+
29+
30+
// MARK: - Configuration
31+
32+
/// A non-operational configuration method.
33+
///
34+
/// - Parameter configuration: The agent configuration (ignored).
35+
func configure(with configuration: any AgentConfigurationProtocol) {}
36+
37+
38+
// MARK: - Sampling
39+
40+
/// A non-operational sampling method.
41+
///
42+
/// - Returns: Always returns `.notSampledOut`.
43+
func sample() -> SamplingDecision {
44+
return .notSampledOut
45+
}
46+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
//
2+
/*
3+
Copyright 2025 Splunk Inc.
4+
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
*/
17+
18+
/// A factory enum providing convenient constructors for common `AgentSessionSampler` instances.
19+
enum SamplerFactory {
20+
21+
/// Creates an agent session sampler that always returns `.notSampledOut`.
22+
///
23+
/// - Returns: An `AgentSessionSampler` instance.
24+
static func alwaysOnSampler() -> AgentSessionSampler {
25+
return AlwaysOnAgentSampler()
26+
}
27+
28+
/// Creates an agent session sampler that always returns `.sampledOut`.
29+
///
30+
/// - Returns: An instance of `NoOpAgentSessionSampler`.
31+
static func alwaysOffSampler() -> AgentSessionSampler {
32+
return AlwaysOffAgentSampler()
33+
}
34+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
//
2+
/*
3+
Copyright 2025 Splunk Inc.
4+
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
*/
17+
18+
/// A protocol for samplers that calculate decisions based on statistical probability.
19+
protocol StatisticalSampler: BaseSampler {
20+
21+
/// The target probability of sampling an item.
22+
var probability: Double { get }
23+
24+
/// The upper bound of the interval used for generating random numbers in the sampling decision.
25+
var upperBound: Double { get }
26+
27+
/// The lower bound of the interval used for generating random numbers in the sampling decision.
28+
var lowerBound: Double { get }
29+
}
30+
31+
extension StatisticalSampler {
32+
33+
// MARK: - Default sampling function implementation
34+
35+
/// Provides a default sampling decision logic for statistical samplers.
36+
///
37+
/// This implementation compares a randomly generated number against the sampler's `probability`.
38+
/// - If `probability` is 1.0, it always returns `.notSampledOut`.
39+
/// - If `probability` is 0.0, it always returns `.sampledOut`.
40+
/// - Otherwise, it generates a random number within the `[lowerBound, upperBound]` range.
41+
/// If this random number is less than or equal to `probability`, it returns `.notSampledOut`, otherwise, it returns `.sampledOut`.
42+
/// For miss-configured bounds, it returns `.sampledOut`.
43+
///
44+
/// - Parameter randomNumberProvider: An object conforming to `RandomNumberProvider`
45+
/// used to generate the random number. Defaults to `SystemRandomNumberProvider()`.
46+
/// - Returns: A `SamplingDecision`.
47+
func sample(randomNumberProvider: RandomNumberProvider = SystemRandomNumberProvider()) -> SamplingDecision {
48+
49+
// Filter out miss-configured bounds.
50+
guard lowerBound <= upperBound, lowerBound >= 0.0, upperBound <= 1.0 else {
51+
return .sampledOut
52+
}
53+
54+
// The user-configured sampling rate is 1, meaning we want to record all Agent sessions.
55+
if probability == 1.0 {
56+
return .notSampledOut
57+
}
58+
59+
// The user-configured sampling rate is 0, meaning we want to record no Agent sessions.
60+
if probability == 0.0 {
61+
return .sampledOut
62+
}
63+
64+
// For any other value, we want to generate a random constant...
65+
let randomNumber = randomNumberProvider.randomNumber(in: lowerBound ... upperBound)
66+
67+
// ... and do a bound comparison against the configured sampling rate.
68+
if randomNumber <= probability {
69+
return .notSampledOut
70+
}
71+
72+
return .sampledOut
73+
}
74+
75+
/// Calculates a sampling decision using the default system random number provider by
76+
/// calling the default `sample(randomNumberProvider: RandomNumberProvider)` function.
77+
///
78+
/// - Returns: A `SamplingDecision`.
79+
func sample() -> SamplingDecision {
80+
return sample(randomNumberProvider: SystemRandomNumberProvider())
81+
}
82+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
//
2+
/*
3+
Copyright 2025 Splunk Inc.
4+
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
*/
17+
18+
/// Defines a sampling decision, calculated as the result of a `BaseSampler`'s `sample()` function call.
19+
enum SamplingDecision {
20+
21+
/// Indicates that the item/session should be sampled out.
22+
case sampledOut
23+
24+
/// Indicates that the item/session should not be sampled out.
25+
case notSampledOut
26+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
//
2+
/*
3+
Copyright 2025 Splunk Inc.
4+
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
*/
17+
18+
/// A specialized `StatisticalSampler` for agent-based session sampling.
19+
///
20+
/// It adds a mechanism to configure the sampler agent-specific session sampling rate.
21+
protocol AgentSessionSampler: StatisticalSampler {
22+
23+
/// Configures the session sampler with agent-specific configuration.
24+
///
25+
/// - Parameter configuration: A `AgentConfigurationProtocol` object which provides
26+
/// the necessary `sessionSamplingRate` configuration.
27+
func configure(with configuration: any AgentConfigurationProtocol)
28+
}

0 commit comments

Comments
 (0)