Skip to content

Conversation

@nickolas-dimitrakas
Copy link
Collaborator

Background

  • Per this doc, Currently, the mParticle Media SDK requires developers to manually manage content time tracking during ad breaks, creating additional complexity and potential for error. If not managed appropriately, the media content time spent value is overstated with ad break time. This PR adds an (optional) ability to allow pausing mediaContentTimeSpent when an Ad Break starts and ends

What Has Changed

  • Added excludeAdBreaksFromContentTime flag to MPMediaSession.
  • Updated logAdBreakStart and logAdBreakEnd to pause/resume content time tracking when the flag is enabled.
  • Added private helpers pauseContentTimeIfAdBreakExclusionEnabled() and resumeContentTimeIfAdBreakExclusionEnabled().
  • Added unit tests for flag behavior and time exclusion accuracy.

Checklist

  • I have performed a self-review of my own code.
  • I have made corresponding changes to the documentation.
  • I have added tests that prove my fix is effective or that my feature works.
  • I have tested this locally.

Additional Notes

Reference Issue (For employees only. Ignore if you are an outside contributor)

@nickolas-dimitrakas nickolas-dimitrakas force-pushed the feat/Update-Media-Content-Time-Spent-Calculations-with-Ad-Breaks branch from 8acc683 to 2031f17 Compare November 6, 2025 16:18
@jamesnrokt jamesnrokt requested a review from a team as a code owner November 6, 2025 17:49
@sonarqubecloud
Copy link

sonarqubecloud bot commented Nov 7, 2025

Copy link
Contributor

@denischilik denischilik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Copy link
Contributor

@BrandonStalnaker BrandonStalnaker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good

@nickolas-dimitrakas nickolas-dimitrakas force-pushed the feat/Update-Media-Content-Time-Spent-Calculations-with-Ad-Breaks branch from f35f72e to 3e48ce1 Compare November 7, 2025 21:34
@nickolas-dimitrakas nickolas-dimitrakas merged commit a12375e into main Nov 7, 2025
5 of 7 checks passed
@nickolas-dimitrakas nickolas-dimitrakas deleted the feat/Update-Media-Content-Time-Spent-Calculations-with-Ad-Breaks branch November 7, 2025 22:31
Copy link

@samdozor samdozor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Likely issues introduced

  • Auto-resume on ad break end can start content time when it wasn’t playing
    • If content was already paused before an ad break starts, ending the ad break will incorrectly start content time tracking.
// MARK: private helpers (ad break)
private func pauseContentTimeIfAdBreakExclusionEnabled() {
    guard excludeAdBreaksFromContentTime, currentPlaybackStartTimestamp != nil else { return }
    storedPlaybackTime += Date().timeIntervalSince(currentPlaybackStartTimestamp!)
    currentPlaybackStartTimestamp = nil
}

private func resumeContentTimeIfAdBreakExclusionEnabled() {
    guard excludeAdBreaksFromContentTime, currentPlaybackStartTimestamp == nil else { return }
    currentPlaybackStartTimestamp = Date()
}
  • Problem: resume... only checks that currentPlaybackStartTimestamp == nil, not that the pause was caused by an ad break. If the user paused manually before the ad break, logAdBreakEnd() will set a new start timestamp and begin counting content time even though playback is still paused.

  • Fix: Track an explicit state flag (e.g., pausedByAdBreak) that is set by pauseContentTimeIfAdBreakExclusionEnabled() and only resume if that flag is true, then clear it.

  • Ad time is double-counted

    • mediaTotalAdTimeSpent is incremented both on ad end/skip and again in the ad summary logger.
@objc public func logAdEnd(options: Options?  = nil) {
    if (self.adContent?.adStartTimestamp != nil) {
        self.adContent?.adEndTimestamp = Date()
        self.adContent?.adCompleted = true
        self.mediaTotalAdTimeSpent += self.adContent!.adEndTimestamp!.timeIntervalSince(self.adContent!.adStartTimestamp!)
    }
    ...
    self.logAdSummary()
}
private func logAdSummary() {
    if (self.adContent != nil) {
        if (self.adContent?.adStartTimestamp != nil) {
            self.adContent?.adEndTimestamp = Date()
            self.mediaTotalAdTimeSpent += self.adContent!.adEndTimestamp!.timeIntervalSince(self.adContent!.adStartTimestamp!)
        }
        ...
    }
}
  • Impact: Ad time gets added twice. Remove the increment in logAdSummary() or skip if already accounted for.

  • Minor: redundant assignment

    • excludeAdBreaksFromContentTime is set twice in the long initializer.
self.excludeAdBreaksFromContentTime = excludeAdBreaksFromContentTime
...
if ( 100 >= completeLimit && completeLimit > 0) {
    self.mediaContentCompleteLimit = completeLimit
}
self.excludeAdBreaksFromContentTime = excludeAdBreaksFromContentTime
  • Fix: Remove the duplicate set.

Tests

  • The new tests properly use short sleeps with tolerances and validate both include/exclude behaviors:
func testAdBreakExclusionDisabledDoesNotPauseContentTime() { ... }
func testAdBreakExclusionEnabledExcludesAdTime() { ... }
  • They are unlikely to be flaky due to the accuracy windows used (0.08–0.1).

  • Add a test covering “ad break while paused” to catch the auto-resume bug:

    • Pause content, start ad break, end ad break, assert currentPlaybackStartTimestamp is still nil and mediaContentTimeSpent unchanged.
  • Optional: Consider removing timing from unit tests by injecting a time source, but the current tolerances are reasonable.

Summary:

  • Ad-break resume should only happen if the ad break actually paused content; add a pausedByAdBreak flag to gate resume.
  • mediaTotalAdTimeSpent is double-counted; remove one of the increments (recommend removing the one in logAdSummary()).
  • Clean up a redundant assignment in the long initializer.
  • Tests look solid with tolerances; add a case for “ad break while paused” to prevent regressions.

@nickolas-dimitrakas
Copy link
Collaborator Author

@samdozor - Thanks for the detailed review and for calling out these issues.

private func logAdSummary() {
    if (self.adContent != nil) {
        if (self.adContent?.adStartTimestamp != nil) {
            self.adContent?.adEndTimestamp = Date()
            self.mediaTotalAdTimeSpent += self.adContent!.adEndTimestamp!.timeIntervalSince(self.adContent!.adStartTimestamp!)
        }
        ...
        customAttributes[adContentStartTimestampKey] = self.adContent?.adStartTimestamp
    }
}

One thing I noticed as well: in the scenario where adContent.adStartTimestamp is nil, I’m wondering whether we should be running any of the logAdSummary logic at all. In the current implementation, logAdSummary() will still set an end timestamp and attempt to track ad time even though there was never a recorded start. It seems like we may want to fully skip the summary logic when adStartTimestamp is nil rather than partially executing it.

cc @BrandonStalnaker

@BrandonStalnaker
Copy link
Contributor

@samdozor - Thanks for the detailed review and for calling out these issues.

private func logAdSummary() {
    if (self.adContent != nil) {
        if (self.adContent?.adStartTimestamp != nil) {
            self.adContent?.adEndTimestamp = Date()
            self.mediaTotalAdTimeSpent += self.adContent!.adEndTimestamp!.timeIntervalSince(self.adContent!.adStartTimestamp!)
        }
        ...
        customAttributes[adContentStartTimestampKey] = self.adContent?.adStartTimestamp
    }
}

One thing I noticed as well: in the scenario where adContent.adStartTimestamp is nil, I’m wondering whether we should be running any of the logAdSummary logic at all. In the current implementation, logAdSummary() will still set an end timestamp and attempt to track ad time even though there was never a recorded start. It seems like we may want to fully skip the summary logic when adStartTimestamp is nil rather than partially executing it.

cc @BrandonStalnaker

That's tough to say because it really depends on how the customer implements this. I'd lean towards logging what we can considering the only places we call the method is if adContent is set and if the class is deallocated or the ad is skipped or ended. In those cases I would assume the customer would want some reference of the ad they initialized even if they're not implementing the timestamps.

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.

6 participants