Skip to content

Commit 611511b

Browse files
authored
Introduce sampling to session events. (#10516)
1 parent 68628ef commit 611511b

File tree

11 files changed

+194
-19
lines changed

11 files changed

+194
-19
lines changed

FirebaseSessions/ProtoSupport/Protos/sessions.proto

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ message DataCollectionStatus {
7979
DataCollectionState crashlytics = 2;
8080
// Any sampling rate being applied to these events on device.
8181
// 1.0 implies no sampling.
82-
float session_sampling_rate = 3;
82+
double session_sampling_rate = 3;
8383
}
8484

8585
// Enum denoting all possible states for SDK data collection.

FirebaseSessions/Protogen/nanopb/sessions.nanopb.c

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ const pb_field_t firebase_appquality_sessions_SessionInfo_fields[6] = {
4545
const pb_field_t firebase_appquality_sessions_DataCollectionStatus_fields[4] = {
4646
PB_FIELD( 1, UENUM , SINGULAR, STATIC , FIRST, firebase_appquality_sessions_DataCollectionStatus, performance, performance, 0),
4747
PB_FIELD( 2, UENUM , SINGULAR, STATIC , OTHER, firebase_appquality_sessions_DataCollectionStatus, crashlytics, performance, 0),
48-
PB_FIELD( 3, FLOAT , SINGULAR, STATIC , OTHER, firebase_appquality_sessions_DataCollectionStatus, session_sampling_rate, crashlytics, 0),
48+
PB_FIELD( 3, DOUBLE , SINGULAR, STATIC , OTHER, firebase_appquality_sessions_DataCollectionStatus, session_sampling_rate, crashlytics, 0),
4949
PB_LAST_FIELD
5050
};
5151

@@ -104,4 +104,10 @@ PB_STATIC_ASSERT((pb_membersize(firebase_appquality_sessions_SessionEvent, sessi
104104
#endif
105105

106106

107+
/* On some platforms (such as AVR), double is really float.
108+
* These are not directly supported by nanopb, but see example_avr_double.
109+
* To get rid of this error, remove any double fields from your .proto.
110+
*/
111+
PB_STATIC_ASSERT(sizeof(double) == 8, DOUBLE_MUST_BE_8_BYTES)
112+
107113
/* @@protoc_insertion_point(eof) */

FirebaseSessions/Protogen/nanopb/sessions.nanopb.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ typedef struct _firebase_appquality_sessions_AppleApplicationInfo {
9393
typedef struct _firebase_appquality_sessions_DataCollectionStatus {
9494
firebase_appquality_sessions_DataCollectionState performance;
9595
firebase_appquality_sessions_DataCollectionState crashlytics;
96-
float session_sampling_rate;
96+
double session_sampling_rate;
9797
/* @@protoc_insertion_point(struct:firebase_appquality_sessions_DataCollectionStatus) */
9898
} firebase_appquality_sessions_DataCollectionStatus;
9999

@@ -182,7 +182,7 @@ extern const pb_field_t firebase_appquality_sessions_AppleApplicationInfo_fields
182182
/* Maximum encoded size of messages (where known) */
183183
/* firebase_appquality_sessions_SessionEvent_size depends on runtime parameters */
184184
/* firebase_appquality_sessions_SessionInfo_size depends on runtime parameters */
185-
#define firebase_appquality_sessions_DataCollectionStatus_size 9
185+
#define firebase_appquality_sessions_DataCollectionStatus_size 13
186186
/* firebase_appquality_sessions_ApplicationInfo_size depends on runtime parameters */
187187
/* firebase_appquality_sessions_AndroidApplicationInfo_size depends on runtime parameters */
188188
/* firebase_appquality_sessions_AppleApplicationInfo_size depends on runtime parameters */

FirebaseSessions/Sources/Development/DevEventConsoleLogger.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class DevEventConsoleLogger: EventGDTLoggerProtocol {
3939
data_collection_status
4040
crashlytics: \(proto.session_data.data_collection_status.crashlytics)
4141
performance: \(proto.session_data.data_collection_status.performance)
42+
session_sampling_rate: \(proto.session_data.data_collection_status.session_sampling_rate)
4243
application_info
4344
app_id: \(proto.application_info.app_id.description)
4445
device_model: \(proto.application_info.device_model.description)

FirebaseSessions/Sources/FirebaseSessions.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,11 @@ protocol SessionsProvider {
5555
let fireLogger = EventGDTLogger(googleDataTransport: googleDataTransport!)
5656

5757
let identifiers = Identifiers(installations: installations)
58-
let coordinator = SessionCoordinator(identifiers: identifiers, fireLogger: fireLogger)
58+
let coordinator = SessionCoordinator(
59+
identifiers: identifiers,
60+
fireLogger: fireLogger,
61+
sampler: SessionSampler()
62+
)
5963
let initiator = SessionInitiator()
6064
let appInfo = ApplicationInfo(appID: appID)
6165
let settings = Settings(appInfo: appInfo)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright 2022 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import Foundation
16+
17+
/// Contains the list of errors that are localized for Firebase Sessions Library
18+
enum FirebaseSessionsError: Error {
19+
/// Event sampling related error
20+
case SessionSamplingError
21+
}

FirebaseSessions/Sources/SessionCoordinator.swift

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,29 +21,40 @@ import Foundation
2121
class SessionCoordinator {
2222
let identifiers: IdentifierProvider
2323
let fireLogger: EventGDTLoggerProtocol
24+
let sampler: SessionSamplerProtocol
2425

25-
init(identifiers: IdentifierProvider, fireLogger: EventGDTLoggerProtocol) {
26+
init(identifiers: IdentifierProvider, fireLogger: EventGDTLoggerProtocol,
27+
sampler: SessionSamplerProtocol) {
2628
self.identifiers = identifiers
2729
self.fireLogger = fireLogger
30+
self.sampler = sampler
2831
}
2932

3033
// Begins the process of logging a SessionStartEvent to FireLog, while taking into account Data Collection, Sampling, and fetching Settings
3134
func attemptLoggingSessionStart(event: SessionStartEvent,
3235
callback: @escaping (Result<Void, Error>) -> Void) {
33-
event.setInstallationID(identifiers: identifiers)
36+
if sampler.shouldSendEventForSession(sessionId: identifiers.sessionID) {
37+
event.setInstallationID(identifiers: identifiers)
3438

35-
fireLogger.logEvent(event: event) { result in
36-
switch result {
37-
case .success():
38-
Logger.logInfo("Successfully logged Session Start event to GoogleDataTransport")
39-
callback(.success(()))
40-
case let .failure(error):
41-
Logger
42-
.logError(
43-
"Error logging Session Start event to GoogleDataTransport: \(error)."
44-
)
45-
callback(.failure(error))
39+
fireLogger.logEvent(event: event) { result in
40+
switch result {
41+
case .success():
42+
Logger.logInfo("Successfully logged Session Start event to GoogleDataTransport")
43+
callback(.success(()))
44+
case let .failure(error):
45+
Logger
46+
.logError(
47+
"Error logging Session Start event to GoogleDataTransport: \(error)."
48+
)
49+
callback(.failure(error))
50+
}
4651
}
52+
} else {
53+
Logger
54+
.logInfo(
55+
"Session event dropped due to sampling."
56+
)
57+
callback(.failure(FirebaseSessionsError.SessionSamplingError))
4758
}
4859
}
4960
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright 2022 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import Foundation
16+
17+
protocol SessionSamplerProtocol {
18+
/// Sampling rate that has to be applied across sessions.
19+
/// Ranges from 0 to 1 in Double.
20+
var sessionSamplingRate: Double { get set }
21+
22+
/// Determines if a provided sessionID should be sampled or not.
23+
/// Note: Sample means allowed. A return of true means the event should be allowed, else dropped.
24+
func shouldSendEventForSession(sessionId: String) -> Bool
25+
}
26+
27+
class SessionSampler: SessionSamplerProtocol {
28+
var sessionSamplingRate: Double
29+
30+
/// TODO: Update this to a sampling logic once we have the configuration flags in place.
31+
/// Currently defaulted to 1.0 where no events are dropped.
32+
init(sessionSamplingRate: Double = 1.0) {
33+
self.sessionSamplingRate = sessionSamplingRate
34+
}
35+
36+
func shouldSendEventForSession(sessionId: String) -> Bool {
37+
let randomValue = Double.random(in: 0 ... 1)
38+
if randomValue > sessionSamplingRate {
39+
return false
40+
}
41+
return true
42+
}
43+
}

FirebaseSessions/Sources/SessionStartEvent.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ class SessionStartEvent: NSObject, GDTCOREventDataObject {
6060
proto.session_data.firebase_installation_id = makeProtoString(identifiers.installationID)
6161
}
6262

63+
func setSamplingRate(samplingRate: Double) {
64+
proto.session_data.data_collection_status.session_sampling_rate = samplingRate
65+
}
66+
6367
// MARK: - GDTCOREventDataObject
6468

6569
func transportBytes() -> Data {

FirebaseSessions/Tests/Unit/SessionCoordinatorTests.swift

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,19 @@ class SessionCoordinatorTests: XCTestCase {
2222
var time = MockTimeProvider()
2323
var fireLogger = MockGDTLogger()
2424
var appInfo = MockApplicationInfo()
25+
var sampler = SessionSampler()
2526

2627
var coordinator: SessionCoordinator!
2728

2829
override func setUp() {
2930
super.setUp()
3031

31-
coordinator = SessionCoordinator(identifiers: identifiers, fireLogger: fireLogger)
32+
coordinator = SessionCoordinator(
33+
identifiers: identifiers,
34+
fireLogger: fireLogger,
35+
sampler: sampler
36+
)
37+
sampler.sessionSamplingRate = 1.0
3238
}
3339

3440
func test_attemptLoggingSessionStart_logsToGDT() throws {
@@ -84,4 +90,43 @@ class SessionCoordinatorTests: XCTestCase {
8490
XCTAssertEqual(fireLogger.loggedEvent, event)
8591
XCTAssertFalse(resultSuccess)
8692
}
93+
94+
func test_eventNotDropped_handlesAllEventsAllowed() throws {
95+
identifiers.mockAllValidIDs()
96+
97+
let event = SessionStartEvent(identifiers: identifiers, appInfo: appInfo, time: time)
98+
99+
sampler.sessionSamplingRate = 1.0
100+
var resultSuccess = true
101+
coordinator.attemptLoggingSessionStart(event: event) { result in
102+
switch result {
103+
case .success(()):
104+
resultSuccess = true
105+
case .failure:
106+
resultSuccess = false
107+
}
108+
}
109+
110+
XCTAssertTrue(resultSuccess)
111+
}
112+
113+
func test_eventDropped_handlesZeroSamplingRate() throws {
114+
identifiers.mockAllValidIDs()
115+
116+
let event = SessionStartEvent(identifiers: identifiers, appInfo: appInfo, time: time)
117+
118+
sampler.sessionSamplingRate = 0.0
119+
120+
var resultSuccess = true
121+
coordinator.attemptLoggingSessionStart(event: event) { result in
122+
switch result {
123+
case .success(()):
124+
resultSuccess = true
125+
case .failure:
126+
resultSuccess = false
127+
}
128+
}
129+
130+
XCTAssertFalse(resultSuccess)
131+
}
87132
}

0 commit comments

Comments
 (0)