From 35511cf6194af34b5c134c47f11f6c327143b036 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 6 Aug 2026 13:14:30 +0000 Subject: [PATCH 1/2] Report THEOads interstitial errors as failed ads to Conviva Co-Authored-By: benny.tepfer --- CHANGELOG.md | 5 +++ Code/Conviva/README.md | 1 + .../Source/Base/ConvivaConnector.swift | 9 +++++ .../Events/ConvivaHandlers/AdHandler.swift | 38 +++++++++++++++++++ .../ConvivaHandlers/AdHandler+THEOads.swift | 25 ++++++++++++ .../Observers/THEOadsEventForwarder.swift | 33 ++++++++++++++++ THEOplayer-Connector-Conviva.podspec | 7 ++++ 7 files changed, 118 insertions(+) create mode 100644 Code/Conviva/Source/THEOads/Events/ConvivaHandlers/AdHandler+THEOads.swift create mode 100644 Code/Conviva/Source/THEOads/Events/Observers/THEOadsEventForwarder.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 80f9dd14..db5f4faa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Fixed + +- Conviva + - Fixed an issue where a THEOads ad break that failed before any ad was available, for example on an empty VAST response, was not reported as an ad attempt. + ## [11.0.4] - 2026-05-29 ### Changed diff --git a/Code/Conviva/README.md b/Code/Conviva/README.md index 99cfca57..cffd79d5 100644 --- a/Code/Conviva/README.md +++ b/Code/Conviva/README.md @@ -26,6 +26,7 @@ To support custom feature builds of THEOplayerSDK perform the following steps: 1. Create a Podfile if you don't already have one. From the root of your project directory, run the following command: `pod init` 2. To your Podfile, add the Conviva connector pods that you want to use in your app: `pod 'THEOplayer-Connector-Conviva'` + - When using THEOads, also add the THEOads subspec so that failed THEOads ad breaks (e.g. an empty VAST response) are reported to Conviva: `pod 'THEOplayer-Connector-Conviva/THEOads'` 3. Install the pods using `pod install` , then open your `.xcworkspace` file to see the project in Xcode. To support custom feature builds of THEOplayerSDK perform the following steps: diff --git a/Code/Conviva/Source/Base/ConvivaConnector.swift b/Code/Conviva/Source/Base/ConvivaConnector.swift index 5b5b8d83..f8b346cc 100644 --- a/Code/Conviva/Source/Base/ConvivaConnector.swift +++ b/Code/Conviva/Source/Base/ConvivaConnector.swift @@ -20,6 +20,10 @@ public class ConvivaConnector { private let theoliveForwarder: THEOliveEventForwarder private let theoliveHandler: THEOliveHandler #endif + +#if canImport(THEOplayerTHEOadsIntegration) + private let theoadsForwarder: THEOadsEventForwarder +#endif public convenience init?( configuration: ConvivaConfiguration, @@ -51,6 +55,11 @@ public class ConvivaConnector { self.theoliveHandler = THEOliveHandler(endpoints: self.endPoints, storage: self.storage) self.theoliveForwarder = THEOliveEventForwarder(player: player, handler: self.theoliveHandler) #endif + +#if canImport(THEOplayerTHEOadsIntegration) + // THEOads level handling + self.theoadsForwarder = THEOadsEventForwarder(player: player, handler: self.adHandler) +#endif } public func destroy() { diff --git a/Code/Conviva/Source/Base/Events/ConvivaHandlers/AdHandler.swift b/Code/Conviva/Source/Base/Events/ConvivaHandlers/AdHandler.swift index e43f00f9..a46bb1d1 100644 --- a/Code/Conviva/Source/Base/Events/ConvivaHandlers/AdHandler.swift +++ b/Code/Conviva/Source/Base/Events/ConvivaHandlers/AdHandler.swift @@ -9,6 +9,8 @@ class AdHandler { static let serializationFormatter: NumberFormatter = createSerializationFormatter() private weak var endpoints: ConvivaEndpoints? private weak var storage: ConvivaStorage? + private var isAdBreakActive: Bool = false + private var failedAdBreakCounter: Int = 0 init(endpoints: ConvivaEndpoints, storage: ConvivaStorage) { self.endpoints = endpoints @@ -82,6 +84,7 @@ class AdHandler { func adBreakBegin(event: AdBreakBeginEvent) { log("handling adBreakBegin") + self.isAdBreakActive = true guard let adBreak = event.ad else { return } let adBreakInfo = [ CIS_SSDK_AD_BREAK_POD_DURATION: Self.serialize(number: .init(value: adBreak.maxDuration)), @@ -99,6 +102,7 @@ class AdHandler { func adBreakEnd(event: AdBreakEndEvent) { log("handling adBreakEnd") + self.isAdBreakActive = false log("videoAnalytics.reportAdBreakEnded") self.endpoints?.videoAnalytics.reportAdBreakEnded() } @@ -174,6 +178,40 @@ class AdHandler { } } + /// Reports an ad break that failed before any ad became available, for example when the ad server + /// returns an empty VAST response for a server-guided (THEOads) ad break. No ad break or ad events + /// are dispatched in that case, so report it here to keep Conviva's ad attempt and fill rate metrics correct. + func reportFailedAdBreak(message: String, podDuration: Double?, isPreRoll: Bool) { + guard !self.isAdBreakActive else { return } + self.failedAdBreakCounter += 1 + let adBreakInfo: [String: Any] = [ + CIS_SSDK_AD_BREAK_POD_DURATION: Self.serialize(number: .init(value: podDuration ?? 0)), + CIS_SSDK_AD_BREAK_POD_INDEX: Self.serialize(number: .init(value: self.failedAdBreakCounter)), + CIS_SSDK_AD_BREAK_POD_POSITION: isPreRoll ? "Pre-roll" : "Mid-roll", + "podTechnology": "Server Guided" + ] + log("videoAnalytics.reportAdBreakStarted: \(adBreakInfo)") + self.endpoints?.videoAnalytics.reportAdBreakStarted( + .ADPLAYER_CONTENT, + adType: .SERVER_SIDE, + adBreakInfo: adBreakInfo + ) + + var info: [String: Any] = ["c3.ad.technology": "Server Guided"] + if let contentAssetName = self.storage?.metadataEntryForKey(CIS_SSDK_METADATA_ASSET_NAME) { + info["contentAssetName"] = contentAssetName + } + if let videoAnalytics = self.endpoints?.videoAnalytics { + info["c3.csid"] = videoAnalytics.getSessionId() + } + log("adAnalytics.setAdInfo: \(info)") + self.endpoints?.adAnalytics.setAdInfo(info) + log("adAnalytics.reportAdFailed: \(message)") + self.endpoints?.adAnalytics.reportAdFailed(message, adInfo: info) + log("videoAnalytics.reportAdBreakEnded") + self.endpoints?.videoAnalytics.reportAdBreakEnded() + } + static func createSerializationFormatter() -> NumberFormatter { let formatter = NumberFormatter() formatter.usesGroupingSeparator = false diff --git a/Code/Conviva/Source/THEOads/Events/ConvivaHandlers/AdHandler+THEOads.swift b/Code/Conviva/Source/THEOads/Events/ConvivaHandlers/AdHandler+THEOads.swift new file mode 100644 index 00000000..a8e6419c --- /dev/null +++ b/Code/Conviva/Source/THEOads/Events/ConvivaHandlers/AdHandler+THEOads.swift @@ -0,0 +1,25 @@ +// +// AdHandler+THEOads.swift +// + +import ConvivaSDK +import THEOplayerSDK + +#if canImport(THEOplayerTHEOadsIntegration) +import THEOplayerTHEOadsIntegration + +extension AdHandler { + /// A THEOads (SGAI) ad break can fail before any ad is available, for example when the ad server + /// returns an empty VAST response. No ad break or ad events are dispatched in that case, so report + /// the attempted ad break as a failed ad to keep Conviva's ad attempt and fill rate metrics correct. + func interstitialError(event: THEOplayerTHEOadsIntegration.InterstitialErrorEvent) { + let interstitial = event.interstitial + guard interstitial.type == .adbreak else { return } + self.reportFailedAdBreak( + message: event.message ?? "No ad available", + podDuration: interstitial.duration, + isPreRoll: interstitial.startTime == 0 + ) + } +} +#endif diff --git a/Code/Conviva/Source/THEOads/Events/Observers/THEOadsEventForwarder.swift b/Code/Conviva/Source/THEOads/Events/Observers/THEOadsEventForwarder.swift new file mode 100644 index 00000000..498ac562 --- /dev/null +++ b/Code/Conviva/Source/THEOads/Events/Observers/THEOadsEventForwarder.swift @@ -0,0 +1,33 @@ +// +// THEOadsEventForwarder.swift +// + +import THEOplayerSDK +import THEOplayerConnectorUtilities +#if canImport(THEOplayerTHEOadsIntegration) +import THEOplayerTHEOadsIntegration + +/// A handle that registers THEOads listeners on a theoplayer and removes them on deinit +class THEOadsEventForwarder { + private let theoadsObserver: DispatchObserver? + + init(player: THEOplayer, handler: AdHandler) { + if let theoads = player.theoads { + self.theoadsObserver = .init( + dispatcher: theoads, + eventListeners: Self.forwardEvents(from: theoads, to: handler) + ) + } else { + self.theoadsObserver = nil + } + } + + static func forwardEvents(from theoads: Dispatcher, to handler: AdHandler) -> [RemovableEventListenerProtocol] { + [ + theoads.addRemovableEventListener(type: THEOadsEventTypes.INTERSTITIAL_ERROR) { event in + handler.interstitialError(event: event) + } + ] + } +} +#endif diff --git a/THEOplayer-Connector-Conviva.podspec b/THEOplayer-Connector-Conviva.podspec index 4e21f0fb..5d83de3b 100644 --- a/THEOplayer-Connector-Conviva.podspec +++ b/THEOplayer-Connector-Conviva.podspec @@ -33,6 +33,13 @@ Pod::Spec.new do |s| live.dependency 'THEOplayer-Integration-THEOlive', "~> 11" end + # --- Subspec: THEOads (+THEOads Conviva) --- + s.subspec 'THEOads' do |theoads| + theoads.source_files = 'Code/Conviva/Source/THEOads/**/*' + theoads.dependency "#{s.name}/Base" + theoads.dependency 'THEOplayer-Integration-THEOads', "~> 11" + end + # Default s.default_subspecs = ['Base', 'THEOlive'] end From 6311f776579e72ff920974c4d4687286a231d391 Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Thu, 13 Aug 2026 12:29:44 -0700 Subject: [PATCH 2/2] Refactor interstitial error reporting per review, align with web - Report the correct POD_POSITION for failed THEOads ad breaks by deriving it from the interstitial start time, adding Post-roll handling instead of defaulting everything non-preroll to Mid-roll. - Use one shared sequential adBreakCounter for POD_INDEX in both the adBreakBegin and failed ad break paths, aligned with the web and Android connectors (previously adBreakBegin reported the time offset in seconds, and failed breaks used a separate counter). - Extract collectBaseAdMetadata() for the c3.csid and contentAssetName tags shared by successful and failed ad reporting, and replace the scattered "Server Guided" literals with the sgaiAdTechnology constant. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../Events/ConvivaHandlers/AdHandler.swift | 64 ++++++++++--------- .../ConvivaHandlers/AdHandler+THEOads.swift | 13 +++- 2 files changed, 47 insertions(+), 30 deletions(-) diff --git a/Code/Conviva/Source/Base/Events/ConvivaHandlers/AdHandler.swift b/Code/Conviva/Source/Base/Events/ConvivaHandlers/AdHandler.swift index a46bb1d1..b994df73 100644 --- a/Code/Conviva/Source/Base/Events/ConvivaHandlers/AdHandler.swift +++ b/Code/Conviva/Source/Base/Events/ConvivaHandlers/AdHandler.swift @@ -7,21 +7,37 @@ import THEOplayerSDK class AdHandler { static let serializationFormatter: NumberFormatter = createSerializationFormatter() + /// The ad technology reported to Conviva for THEOads (SGAI). + /// SGAI isn't officially supported by Conviva yet, so we report it with our own string for now. + static let sgaiAdTechnology = "Server Guided" private weak var endpoints: ConvivaEndpoints? private weak var storage: ConvivaStorage? private var isAdBreakActive: Bool = false - private var failedAdBreakCounter: Int = 0 - + private var adBreakCounter: Int = 0 + init(endpoints: ConvivaEndpoints, storage: ConvivaStorage) { self.endpoints = endpoints self.storage = storage } - + func setAdInfo(_ adInfo: [String: Any]) { log("adAnalytics.setAdInfo: \(adInfo)") self.endpoints?.adAnalytics.setAdInfo(adInfo) } - + + /// Ad metadata shared between successful and failed ad reporting: the tags needed to attach + /// the ad session to the content session (`c3.csid` and `contentAssetName`). + private func collectBaseAdMetadata() -> [String: Any] { + var info: [String: Any] = [:] + if let contentAssetName = self.storage?.metadataEntryForKey(CIS_SSDK_METADATA_ASSET_NAME) { + info["contentAssetName"] = contentAssetName + } + if let videoAnalytics = self.endpoints?.videoAnalytics { + info["c3.csid"] = videoAnalytics.getSessionId() + } + return info + } + private func calculatedAdTechnology(_ integrationKind: AdIntegrationKind) -> AdTechnology { switch integrationKind { case AdIntegrationKind.theoads: @@ -33,10 +49,10 @@ class AdHandler { return .SERVER_SIDE } } - + private func AdTechnologyAsString(_ integration: AdIntegrationKind) -> String { if integration == AdIntegrationKind.theoads { - return "Server Guided" + return Self.sgaiAdTechnology } let adTechnology = self.calculatedAdTechnology(integration) switch adTechnology { @@ -86,9 +102,10 @@ class AdHandler { log("handling adBreakBegin") self.isAdBreakActive = true guard let adBreak = event.ad else { return } + self.adBreakCounter += 1 let adBreakInfo = [ CIS_SSDK_AD_BREAK_POD_DURATION: Self.serialize(number: .init(value: adBreak.maxDuration)), - CIS_SSDK_AD_BREAK_POD_INDEX: Self.serialize(number: .init(value: adBreak.timeOffset)), + CIS_SSDK_AD_BREAK_POD_INDEX: Self.serialize(number: .init(value: self.adBreakCounter)), CIS_SSDK_AD_BREAK_POD_POSITION: adBreak.calculateCurrentAdBreakPosition(), "podTechnology": self.AdTechnologyAsString(adBreak.integration) ] @@ -116,16 +133,10 @@ class AdHandler { let adTechnology = self.AdTechnologyAsString(ad.integration) // set Ad technology info["c3.ad.technology"] = adTechnology - - // set Ad contentAssetName - if let contentAssetName = self.storage?.metadataEntryForKey(CIS_SSDK_METADATA_ASSET_NAME) { - info["contentAssetName"] = contentAssetName - } - // set Ad session ID - if let videoAnalytics = self.endpoints?.videoAnalytics { - info["c3.csid"] = videoAnalytics.getSessionId() - } - + + // attach the ad session to the content session + self.collectBaseAdMetadata().forEach { info[$0.key] = $0.value } + // Temporary workaround for missing LinearAd in Native THEOplayerGoogleIMAIntegration. Can be removed after THEO-10161 is completed. if !info.keys.contains(CIS_SSDK_METADATA_IS_LIVE), let duration = event.duration { if duration.isInfinite { @@ -181,14 +192,14 @@ class AdHandler { /// Reports an ad break that failed before any ad became available, for example when the ad server /// returns an empty VAST response for a server-guided (THEOads) ad break. No ad break or ad events /// are dispatched in that case, so report it here to keep Conviva's ad attempt and fill rate metrics correct. - func reportFailedAdBreak(message: String, podDuration: Double?, isPreRoll: Bool) { + func reportFailedAdBreak(message: String, podDuration: Double?, podPosition: String) { guard !self.isAdBreakActive else { return } - self.failedAdBreakCounter += 1 + self.adBreakCounter += 1 let adBreakInfo: [String: Any] = [ CIS_SSDK_AD_BREAK_POD_DURATION: Self.serialize(number: .init(value: podDuration ?? 0)), - CIS_SSDK_AD_BREAK_POD_INDEX: Self.serialize(number: .init(value: self.failedAdBreakCounter)), - CIS_SSDK_AD_BREAK_POD_POSITION: isPreRoll ? "Pre-roll" : "Mid-roll", - "podTechnology": "Server Guided" + CIS_SSDK_AD_BREAK_POD_INDEX: Self.serialize(number: .init(value: self.adBreakCounter)), + CIS_SSDK_AD_BREAK_POD_POSITION: podPosition, + "podTechnology": Self.sgaiAdTechnology ] log("videoAnalytics.reportAdBreakStarted: \(adBreakInfo)") self.endpoints?.videoAnalytics.reportAdBreakStarted( @@ -197,13 +208,8 @@ class AdHandler { adBreakInfo: adBreakInfo ) - var info: [String: Any] = ["c3.ad.technology": "Server Guided"] - if let contentAssetName = self.storage?.metadataEntryForKey(CIS_SSDK_METADATA_ASSET_NAME) { - info["contentAssetName"] = contentAssetName - } - if let videoAnalytics = self.endpoints?.videoAnalytics { - info["c3.csid"] = videoAnalytics.getSessionId() - } + var info = self.collectBaseAdMetadata() + info["c3.ad.technology"] = Self.sgaiAdTechnology log("adAnalytics.setAdInfo: \(info)") self.endpoints?.adAnalytics.setAdInfo(info) log("adAnalytics.reportAdFailed: \(message)") diff --git a/Code/Conviva/Source/THEOads/Events/ConvivaHandlers/AdHandler+THEOads.swift b/Code/Conviva/Source/THEOads/Events/ConvivaHandlers/AdHandler+THEOads.swift index a8e6419c..7e3d7814 100644 --- a/Code/Conviva/Source/THEOads/Events/ConvivaHandlers/AdHandler+THEOads.swift +++ b/Code/Conviva/Source/THEOads/Events/ConvivaHandlers/AdHandler+THEOads.swift @@ -18,8 +18,19 @@ extension AdHandler { self.reportFailedAdBreak( message: event.message ?? "No ad available", podDuration: interstitial.duration, - isPreRoll: interstitial.startTime == 0 + podPosition: Self.calculateInterstitialAdBreakPosition(startTime: interstitial.startTime) ) } + + /// The position of a THEOads interstitial based on its start time. + static func calculateInterstitialAdBreakPosition(startTime: Double) -> String { + if startTime == 0 { + return "Pre-roll" + } else if startTime < 0 || !startTime.isFinite { + return "Post-roll" + } else { + return "Mid-roll" + } + } } #endif