Skip to content

fix: update unattributed outcomes to match attributed outcomes and Android SDK#1655

Open
nan-li wants to merge 2 commits intomainfrom
fix/update_unattributed_outcomes_to_match_attributed
Open

fix: update unattributed outcomes to match attributed outcomes and Android SDK#1655
nan-li wants to merge 2 commits intomainfrom
fix/update_unattributed_outcomes_to_match_attributed

Conversation

@nan-li
Copy link
Copy Markdown
Contributor

@nan-li nan-li commented Mar 24, 2026

Description

One Line Summary

Align iOS session duration reporting (both attributed and unattributed) with Android SDK behavior by deferring all sends behind a 30-second timer.

Details

Motivation

Previously, attributed and unattributed session processors behaved differently:

  • Attributed: Sent session time to Update User on every background event with just the current interval, and deferred the outcomes/measure call behind a 30-second timer. This is so the outcome event only sends at the end of a session rather than on every background.
  • Unattributed: Required 60 seconds of accumulated time before sending anything, then sent the session time to Update User (with the full total) and the outcomes/measure call immediately with no delay. This means outcomes can be sent multiple times for a single session. Analytics would be overcounted for unattributed session counts.

This caused inconsistency between attribution types and also it diverged from the Android SDK, which uses a single model: send both the user property update and the outcomes call together once after a 30-second timer confirms the user has left the app, with no minimum accumulation time required.

Scope

What changed:

OSUnattributedFocusTimeProcessor:

  • Added a 30-second NSTimer delay before sending to outcomes/measure (matching attributed)
  • Sends immediately (no delay) when a session ends via influence change (onSessionEnded)
  • Implemented cancelDelayedJob to invalidate the timer when the user returns to foreground within 30 seconds
  • Moved sendSessionTime from sendOnFocusCall: to the final send method, so it fires once with the full accumulated total (not on every background)
  • Removed the 60-second minimum accumulation threshold
  • Unsent time is now only cleared on outcomes success (retried on next open if it fails)

OSAttributedFocusTimeProcessor:

  • Moved sendSessionTime from sendOnFocusCall: to sendBackgroundAttributedSessionTimeWithParams:, so it fires once with the full accumulated total when the 30-second timer fires or the session ends

Resulting behavior (same for both attributed and unattributed)

  1. User backgrounds the app → time interval is accumulated and persisted, no network calls
  2. If user returns within 30 seconds → timer is cancelled, accumulation continues on next background
  3. If 30 seconds pass in background → both sendSessionTime (user property update) and outcomes/measure are sent together with the full accumulated session total
  4. If session ends due to influence change → both calls are sent immediately with the full accumulated total

Testing

Unit testing

Manual testing

Tested attributed and unattributed session flows by foregrounding/backgrounding the app and verifying:

  • No network calls on background events within 30 seconds
  • Both sendSessionTime and outcomes/measure fire together after 30 seconds with the correct accumulated total
  • Timer is cancelled and no calls are made when returning within 30 seconds
  • Immediate send when session ends due to influence change

Affected code checklist

  • Notifications
    • Display
    • Open
    • Push Processing
    • Confirm Deliveries
  • Outcomes
  • Sessions
  • In-App Messaging
  • REST API requests
  • Public API changes

Checklist

Overview

  • I have filled out all REQUIRED sections above
  • PR does one thing
  • Any Public API changes are explained in the PR details and conform to existing APIs

Testing

  • I have included test coverage for these changes, or explained why they are not needed
  • All automated tests pass, or I explained why that is not possible
  • I have personally tested this on my device, or explained why that is not possible

Final pass

  • Code is as readable as possible.
  • I have reviewed this PR myself, ensuring it meets each checklist item

nan-li added 2 commits March 23, 2026 22:56
Both attributed and unattributed focus time processors now behave
like the Android SDK: session time accumulates silently across
background events, and both the user property update and the
outcomes/measure call are sent together only after a 30-second
timer fires (confirming the user has left). If the user returns
within 30 seconds, the timer is cancelled and no calls are made.

Changes to OSAttributedFocusTimeProcessor:
- Move sendSessionTime from sendOnFocusCall to the actual send
  method so it only fires when the 30s timer fires or the session
  ends, sending the full accumulated total instead of each interval

Changes to OSUnattributedFocusTimeProcessor:
- Add 30s NSTimer delay before sending to outcomes/measure
- Send immediately (no delay) when session ends via influence change
- Implement cancelDelayedJob to invalidate timer on foreground return
- Clear unsent time only on outcomes success (retry on failure)
- Move sendSessionTime to the actual send method (same as attributed)
- Reduce min session threshold from 60s to 1s to match attributed

Made-with: Cursor
Both processors now use a unified `< 1` guard in sendOnFocusCallWithParams: instead of the old getMinSessionTime/hasMinSyncTime dispatch through the base class.
@nan-li nan-li requested a review from a team March 24, 2026 15:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant