Skip to content

Commit 666b761

Browse files
authored
feat: send unattributed sessions to /outcomes/measure (#1648)
* feat: send unattributed sessions to /outcomes/measure When the SDK was migrated from the player model, the /players/{id}/on_focus call was split into two requests: session time via Update User and session duration outcomes via `/outcomes/measure`. The outcomes half was only wired up for attributed sessions, so unattributed sessions never sent to `/outcomes/measure`. However, to get unattributed session data in the dashboard, unattributed sessions needed to be sent to `/outcomes/measure` as well. - Remove END_SESSION guard in OSFocusTimeProcessorFactory that prevented creating an unattributed processor for session-end events - Add sendSessionEndOutcomes call to OSUnattributedFocusTimeProcessor alongside sendSessionTime, matching the old model where both were always sent together * test: add unit tests for OSRequestSendSessionEndOutcomes request builder Verify the request parameters for unattributed (notification_ids omitted, direct: false), attributed direct (direct: true), and attributed indirect (direct: false) influence types.
1 parent 3092003 commit 666b761

9 files changed

Lines changed: 143 additions & 11 deletions

File tree

iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
3C30FE362F21FBE1001B9C25 /* EarlyTriggerTrackingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C30FE352F21FBE1001B9C25 /* EarlyTriggerTrackingTests.swift */; };
8080
3C3D34E92E95EAA5006A2924 /* LiveActivityConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C3D34E82E95EAA5006A2924 /* LiveActivityConstants.swift */; };
8181
3C3D8D782E92DB7500C3E977 /* OSLiveActivityViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C3D8D772E92DB7500C3E977 /* OSLiveActivityViewExtensions.swift */; };
82+
3C4319092F4CE9D90075492D /* SessionEndOutcomesRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C4319082F4CE9D90075492D /* SessionEndOutcomesRequestTests.swift */; };
8283
3C44673E296D099D0039A49E /* OneSignalMobileProvision.m in Sources */ = {isa = PBXBuildFile; fileRef = 912411FD1E73342200E41FD7 /* OneSignalMobileProvision.m */; };
8384
3C44673F296D09CC0039A49E /* OneSignalMobileProvision.h in Headers */ = {isa = PBXBuildFile; fileRef = 912411FC1E73342200E41FD7 /* OneSignalMobileProvision.h */; settings = {ATTRIBUTES = (Public, ); }; };
8485
3C448B9D2936ADFD002F96BC /* OSBackgroundTaskHandlerImpl.h in Headers */ = {isa = PBXBuildFile; fileRef = 3C448B9B2936ADFD002F96BC /* OSBackgroundTaskHandlerImpl.h */; };
@@ -1318,6 +1319,7 @@
13181319
3C30FE352F21FBE1001B9C25 /* EarlyTriggerTrackingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EarlyTriggerTrackingTests.swift; sourceTree = "<group>"; };
13191320
3C3D34E82E95EAA5006A2924 /* LiveActivityConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityConstants.swift; sourceTree = "<group>"; };
13201321
3C3D8D772E92DB7500C3E977 /* OSLiveActivityViewExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLiveActivityViewExtensions.swift; sourceTree = "<group>"; };
1322+
3C4319082F4CE9D90075492D /* SessionEndOutcomesRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionEndOutcomesRequestTests.swift; sourceTree = "<group>"; };
13211323
3C448B9B2936ADFD002F96BC /* OSBackgroundTaskHandlerImpl.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OSBackgroundTaskHandlerImpl.h; sourceTree = "<group>"; };
13221324
3C448B9C2936ADFD002F96BC /* OSBackgroundTaskHandlerImpl.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OSBackgroundTaskHandlerImpl.m; sourceTree = "<group>"; };
13231325
3C448BA12936B474002F96BC /* OSBackgroundTaskManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSBackgroundTaskManager.swift; sourceTree = "<group>"; };
@@ -2586,6 +2588,7 @@
25862588
3C2C7DC2288E007E0020F9AE /* UnitTests-Bridging-Header.h */,
25872589
4746E2A62B86B64100D6324C /* LiveActivitiesSwiftTests.swift */,
25882590
4746E2AA2B8775C400D6324C /* LiveActivitiesObjcTests.m */,
2591+
3C4319082F4CE9D90075492D /* SessionEndOutcomesRequestTests.swift */,
25892592
);
25902593
path = UnitTests;
25912594
sourceTree = "<group>";
@@ -4531,6 +4534,7 @@
45314534
4529DED21FA81EA800CEAB1D /* NSObjectOverrider.m in Sources */,
45324535
CA42CAC320D99CB90001F2F2 /* ProvisionalAuthorizationTests.m in Sources */,
45334536
5B58E4F8237CE7B4009401E0 /* UIDeviceOverrider.m in Sources */,
4537+
3C4319092F4CE9D90075492D /* SessionEndOutcomesRequestTests.swift in Sources */,
45344538
CA8E19022193C6B0009DA223 /* InAppMessagingIntegrationTests.m in Sources */,
45354539
CAB4112B20852E4C005A70D1 /* DelayedConsentInitializationParameters.m in Sources */,
45364540
7AECE59223674A9700537907 /* OSAttributedFocusTimeProcessor.m in Sources */,

iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ typedef enum {ATTRIBUTED, NOT_ATTRIBUTED} FocusAttributionState;
200200
#define focusAttributionStateString(enum) [@[@"ATTRIBUTED", @"NOT_ATTRIBUTED"] objectAtIndex:enum]
201201

202202
// OneSignal Background Task Identifiers
203-
#define ATTRIBUTED_FOCUS_TASK @"ATTRIBUTED_FOCUS_TASK"
203+
#define SESSION_OUTCOMES_TASK @"SESSION_OUTCOMES_TASK"
204204
#define OPERATION_REPO_BACKGROUND_TASK @"OPERATION_REPO_BACKGROUND_TASK"
205205
#define IDENTITY_EXECUTOR_BACKGROUND_TASK @"IDENTITY_EXECUTOR_BACKGROUND_TASK_"
206206
#define PROPERTIES_EXECUTOR_BACKGROUND_TASK @"PROPERTIES_EXECUTOR_BACKGROUND_TASK_"

iOS_SDK/OneSignalSDK/OneSignalOutcomes/Source/OutcomeEvents/OneSignalOutcomeEventsController.m

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,12 +117,12 @@ - (void)sendSessionEndOutcomes:(NSNumber * _Nonnull)timeElapsed
117117
pushSubscriptionId:pushSubscriptionId
118118
onesignalId:onesignalId
119119
influenceParams:influenceParams] onSuccess:^(NSDictionary *result) {
120-
[OneSignalLog onesignalLog:ONE_S_LL_DEBUG message:@"OneSignalOutcomeEventsController:sendSessionEndOutcomes attributed succeed"];
120+
[OneSignalLog onesignalLog:ONE_S_LL_DEBUG message:@"OneSignalOutcomeEventsController:sendSessionEndOutcomes succeed"];
121121
if (successBlock) {
122122
successBlock(result);
123123
}
124124
} onFailure:^(OneSignalClientError *error) {
125-
[OneSignalLog onesignalLog:ONE_S_LL_ERROR message:@"OneSignalOutcomeEventsController:sendSessionEndOutcomes attributed failed"];
125+
[OneSignalLog onesignalLog:ONE_S_LL_ERROR message:@"OneSignalOutcomeEventsController:sendSessionEndOutcomes failed"];
126126
if (failureBlock) {
127127
failureBlock(error.underlyingError);
128128
}

iOS_SDK/OneSignalSDK/Source/OSAttributedFocusTimeProcessor.m

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ @implementation OSAttributedFocusTimeProcessor {
4444

4545
- (instancetype)init {
4646
self = [super init];
47-
[OSBackgroundTaskManager setTaskInvalid:ATTRIBUTED_FOCUS_TASK];
47+
[OSBackgroundTaskManager setTaskInvalid:SESSION_OUTCOMES_TASK];
4848
return self;
4949
}
5050

@@ -85,7 +85,7 @@ - (void)sendOnFocusCallWithParams:(OSFocusCallParams *)params totalTimeActive:(N
8585
return;
8686
}
8787

88-
[OSBackgroundTaskManager beginBackgroundTask:ATTRIBUTED_FOCUS_TASK];
88+
[OSBackgroundTaskManager beginBackgroundTask:SESSION_OUTCOMES_TASK];
8989

9090
if (params.onSessionEnded) {
9191
[self sendBackgroundAttributedSessionTimeWithParams:params withTotalTimeActive:@(totalTimeActive)];
@@ -114,10 +114,10 @@ - (void)sendBackgroundAttributedSessionTimeWithParams:(OSFocusCallParams *)param
114114
[OneSignal sendSessionEndOutcomes:totalTimeActive params:params onSuccess:^(NSDictionary *result) {
115115
[OneSignalLog onesignalLog:ONE_S_LL_DEBUG message:@"sendBackgroundAttributed succeed"];
116116
[super saveUnsentActiveTime:0];
117-
[OSBackgroundTaskManager endBackgroundTask:ATTRIBUTED_FOCUS_TASK];
117+
[OSBackgroundTaskManager endBackgroundTask:SESSION_OUTCOMES_TASK];
118118
} onFailure:^(NSError *error) {
119119
[OneSignalLog onesignalLog:ONE_S_LL_ERROR message:@"sendBackgroundAttributed failed, will retry on next open"];
120-
[OSBackgroundTaskManager endBackgroundTask:ATTRIBUTED_FOCUS_TASK];
120+
[OSBackgroundTaskManager endBackgroundTask:SESSION_OUTCOMES_TASK];
121121
}];
122122
});
123123
}
@@ -128,7 +128,7 @@ - (void)cancelDelayedJob {
128128

129129
[restCallTimer invalidate];
130130
restCallTimer = nil;
131-
[OSBackgroundTaskManager endBackgroundTask:ATTRIBUTED_FOCUS_TASK];
131+
[OSBackgroundTaskManager endBackgroundTask:SESSION_OUTCOMES_TASK];
132132
}
133133

134134
@end

iOS_SDK/OneSignalSDK/Source/OSFocusTimeProcessorFactory.m

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,6 @@ + (OSBaseFocusTimeProcessor *)createTimeProcessorWithInfluences:(NSArray<OSInflu
7676
timeProcesor = [OSAttributedFocusTimeProcessor new];
7777
break;
7878
case NOT_ATTRIBUTED:
79-
// TODO: Clean up, this check should be for getting and not create
80-
if (focusEventType == END_SESSION)
81-
break;
8279
timeProcesor = [OSUnattributedFocusTimeProcessor new];
8380
break;
8481
}

iOS_SDK/OneSignalSDK/Source/OSUnattributedFocusTimeProcessor.m

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,21 @@
2828
#import <OneSignalCore/OneSignalCore.h>
2929
#import <OneSignalOSCore/OneSignalOSCore.h>
3030
#import "OSMacros.h"
31+
#import "OneSignalFramework.h"
3132
#import "OSUnattributedFocusTimeProcessor.h"
3233
#import <OneSignalUser/OneSignalUser.h>
3334

35+
@interface OneSignal ()
36+
+ (void)sendSessionEndOutcomes:(NSNumber*)totalTimeActive params:(OSFocusCallParams *)params onSuccess:(OSResultSuccessBlock _Nonnull)successBlock onFailure:(OSFailureBlock _Nonnull)failureBlock;
37+
@end
38+
3439
@implementation OSUnattributedFocusTimeProcessor
3540

3641
static let UNATTRIBUTED_MIN_SESSION_TIME_SEC = 60;
3742

3843
- (instancetype)init {
3944
self = [super init];
45+
[OSBackgroundTaskManager setTaskInvalid:SESSION_OUTCOMES_TASK];
4046
return self;
4147
}
4248

@@ -74,6 +80,17 @@ - (void)sendOnFocusCallWithParams:(OSFocusCallParams *)params totalTimeActive:(N
7480
[OneSignalLog onesignalLog:ONE_S_LL_DEBUG message:[NSString stringWithFormat:@"OSUnattributedFocusTimeProcessor:sendSessionTime of %@", @(totalTimeActive)]];
7581
[OneSignalUserManagerImpl.sharedInstance sendSessionTime:@(totalTimeActive)];
7682
[super saveUnsentActiveTime:0];
83+
84+
[OSBackgroundTaskManager beginBackgroundTask:SESSION_OUTCOMES_TASK];
85+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
86+
[OneSignal sendSessionEndOutcomes:@(totalTimeActive) params:params onSuccess:^(NSDictionary *result) {
87+
[OneSignalLog onesignalLog:ONE_S_LL_DEBUG message:@"sendUnattributed session end outcomes succeed"];
88+
[OSBackgroundTaskManager endBackgroundTask:SESSION_OUTCOMES_TASK];
89+
} onFailure:^(NSError *error) {
90+
[OneSignalLog onesignalLog:ONE_S_LL_ERROR message:@"sendUnattributed session end outcomes failed"];
91+
[OSBackgroundTaskManager endBackgroundTask:SESSION_OUTCOMES_TASK];
92+
}];
93+
});
7794
}
7895

7996
- (void)cancelDelayedJob {
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# in tests, we may want to force cast and throw any errors
2+
disabled_rules:
3+
- force_cast
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
Modified MIT License
3+
4+
Copyright 2026 OneSignal
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
1. The above copyright notice and this permission notice shall be included in
14+
all copies or substantial portions of the Software.
15+
16+
2. All copies of substantial portions of the Software may only be used in connection
17+
with services provided by OneSignal.
18+
19+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25+
THE SOFTWARE.
26+
*/
27+
28+
import XCTest
29+
import OneSignalOutcomes
30+
31+
class SessionEndOutcomesRequestTests: XCTestCase {
32+
33+
func testUnattributedInfluence() {
34+
let influenceParam = OSFocusInfluenceParam(
35+
paramsInfluenceIds: nil,
36+
influenceKey: "notification_ids",
37+
directInfluence: false,
38+
influenceDirectKey: "direct"
39+
)!
40+
41+
let request = OSRequestSendSessionEndOutcomes.withActiveTime(
42+
120,
43+
appId: "test-app-id",
44+
pushSubscriptionId: "test-push-sub-id",
45+
onesignalId: "test-onesignal-id",
46+
influenceParams: [influenceParam]
47+
)
48+
49+
XCTAssertEqual(request.path, "outcomes/measure")
50+
XCTAssertEqual(request.method, POST)
51+
52+
let params = request.parameters as! [String: Any]
53+
XCTAssertEqual(params["app_id"] as? String, "test-app-id")
54+
XCTAssertEqual(params["id"] as? String, "os__session_duration")
55+
XCTAssertEqual(params["session_time"] as? Int, 120)
56+
XCTAssertEqual(params["onesignal_id"] as? String, "test-onesignal-id")
57+
58+
let subscription = params["subscription"] as! [String: Any]
59+
XCTAssertEqual(subscription["id"] as? String, "test-push-sub-id")
60+
XCTAssertEqual(subscription["type"] as? String, "iOSPush")
61+
62+
XCTAssertEqual(params["direct"] as? Bool, false)
63+
XCTAssertNil(params["notification_ids"])
64+
}
65+
66+
func testAttributedDirectInfluence() {
67+
let notificationIds = ["notif-1", "notif-2"]
68+
let influenceParam = OSFocusInfluenceParam(
69+
paramsInfluenceIds: notificationIds,
70+
influenceKey: "notification_ids",
71+
directInfluence: true,
72+
influenceDirectKey: "direct"
73+
)!
74+
75+
let request = OSRequestSendSessionEndOutcomes.withActiveTime(
76+
60,
77+
appId: "test-app-id",
78+
pushSubscriptionId: "test-push-sub-id",
79+
onesignalId: "test-onesignal-id",
80+
influenceParams: [influenceParam]
81+
)
82+
83+
let params = request.parameters as! [String: Any]
84+
XCTAssertEqual(params["direct"] as? Bool, true)
85+
XCTAssertEqual(params["notification_ids"] as? [String], notificationIds)
86+
XCTAssertEqual(params["session_time"] as? Int, 60)
87+
}
88+
89+
func testAttributedIndirectInfluence() {
90+
let notificationIds = ["notif-1", "notif-2", "notif-3"]
91+
let influenceParam = OSFocusInfluenceParam(
92+
paramsInfluenceIds: notificationIds,
93+
influenceKey: "notification_ids",
94+
directInfluence: false,
95+
influenceDirectKey: "direct"
96+
)!
97+
98+
let request = OSRequestSendSessionEndOutcomes.withActiveTime(
99+
90,
100+
appId: "test-app-id",
101+
pushSubscriptionId: "test-push-sub-id",
102+
onesignalId: "test-onesignal-id",
103+
influenceParams: [influenceParam]
104+
)
105+
106+
let params = request.parameters as! [String: Any]
107+
XCTAssertEqual(params["direct"] as? Bool, false)
108+
XCTAssertEqual(params["notification_ids"] as? [String], notificationIds)
109+
}
110+
}

iOS_SDK/OneSignalSDK/UnitTests/UnitTests-Bridging-Header.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
//
44

55
#import "OneSignalFramework.h"
6+
#import "OSOutcomesRequests.h"

0 commit comments

Comments
 (0)