Skip to content

Commit

Permalink
Support for Native Impression Tracking (#1060)
Browse files Browse the repository at this point in the history
* feat: impression tracking for banner

* feat: observe interstitial apperance

* feat: some improvements

* feat: add interstitial impression tracking

* refactor: clean up

* fix: fix calling timer on background thread

* refactor: simplify AdViewUtils class

* feat:  implement cache id search

* feat: make AdViewUtils class more generic

* feat: add additional check of hb_cache_id tp ensure that we found Prebid creative

* feat: update tests

* feat: minor changes

* feat: add ability to pass ad view in PrebidAdUnit

* feat: update tests

* feat: move fetchDemand with adView to BannerAdUnit

* feat: add tests for BannerViewReloadTracker

* feat: update tests

* refactor: minor corrections

* feat: update scripts

* feat: disable parallel test execution & run trackers only for banner ads

* feat: update AdViewUtilsTests

* feat: update timeout

* feat: minor changes

* feat: change pollingInterval

* feat: add activatePrebidImpressionTracker flag for interstitials

* feat: introduce activatePrebidImpressionTracker method

* feat: move impression tracking logic to PrebidImpressionTracker

* refactor: transform activatePrebidImpressionTracker to method for consistency

* fix: small update for InternalTestApp

* test: increase intervals for some tests

* feat: update Podfile.lock

* feat: resolve duplicats
  • Loading branch information
OlenaPostindustria authored Feb 18, 2025
1 parent 96c8a5a commit 9947afd
Show file tree
Hide file tree
Showing 30 changed files with 1,206 additions and 328 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ class PrebidOriginalAPIDisplayBannerController:
rootController?.bannerView?.addSubview(gamBanner)

let gamRequest = GAMRequest()
adUnit.activatePrebidImpressionTracker(adView: gamBanner)
adUnit.fetchDemand(adObject: gamRequest) { [weak self] resultCode in
Log.info("Prebid demand fetch for GAM \(resultCode.name())")
self?.gamBanner.load(gamRequest)
Expand Down Expand Up @@ -164,6 +165,7 @@ class PrebidOriginalAPIDisplayBannerController:
resetEvents()

let gamRequest = GAMRequest()
adUnit.activatePrebidImpressionTracker(adView: gamBanner)
adUnit.fetchDemand(adObject: gamRequest) { [weak self] resultCode in
Log.info("Prebid demand fetch for GAM \(resultCode.name())")
self?.gamBanner.load(gamRequest)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,13 @@ class PrebidOriginalAPIDisplayInterstitialController:
configIdLabel.isHidden = false
configIdLabel.text = "Config ID: \(prebidConfigId)"

adUnit = InterstitialAdUnit(configId: prebidConfigId, minWidthPerc: 50, minHeightPerc: 70)
adUnit = InterstitialAdUnit(
configId: prebidConfigId,
minWidthPerc: 50,
minHeightPerc: 70
)

adUnit.activatePrebidImpressionTracker()

// imp[].ext.data
if let adUnitContext = AppConfiguration.shared.adUnitContext {
Expand Down
3 changes: 2 additions & 1 deletion Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ DEPENDENCIES:
- AppLovinSDK
- Eureka
- Google-Mobile-Ads-SDK
- Google-Mobile-Ads-SDK (<= 11.13.0)
- GoogleAds-IMA-iOS-SDK
- RxSwift
- SVProgressHUD
Expand All @@ -41,6 +42,6 @@ SPEC CHECKSUMS:
RxSwift: 4e28be97cbcfeee614af26d83415febbf2bf6f45
SVProgressHUD: 4837c74bdfe2e51e8821c397825996a8d7de6e22

PODFILE CHECKSUM: bae4436ed691a1d2217fde386d8881d6e7e06963
PODFILE CHECKSUM: df50580a1f6c422298edf0513ce00e759ea4668a

COCOAPODS: 1.16.2
62 changes: 57 additions & 5 deletions PrebidMobile.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

38 changes: 33 additions & 5 deletions PrebidMobile/AdUnits/AdUnit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public class AdUnit: NSObject, DispatcherDelegate {
private var isInitialFetchDemandCallMade = false

private var adServerObject: AnyObject?

private var lastFetchDemandCompletion: ((_ bidInfo: BidInfo) -> Void)?

/// notification flag set to check if the prebid response is received within the specified time
Expand All @@ -53,6 +54,9 @@ public class AdUnit: NSObject, DispatcherDelegate {
/// notification flag set to determine if delegate call needs to be made after timeout delegate is sent
private var timeOutSignalSent = false

private(set) lazy var impressionTracker = PrebidImpressionTracker(
isInterstitial: adUnitConfig.adConfiguration.isInterstitialAd
)

/// Initializes a new `AdUnit` instance with the specified configuration ID, size, and ad formats.
///
Expand All @@ -65,8 +69,12 @@ public class AdUnit: NSObject, DispatcherDelegate {
adUnitConfig.adConfiguration.isOriginalAPI = true
adUnitConfig.adFormats = adFormats

bidRequester = PBMBidRequester(connection: PrebidServerConnection.shared, sdkConfiguration: Prebid.shared,
targeting: Targeting.shared, adUnitConfiguration: adUnitConfig)
bidRequester = PBMBidRequester(
connection: PrebidServerConnection.shared,
sdkConfiguration: Prebid.shared,
targeting: Targeting.shared,
adUnitConfiguration: adUnitConfig
)

super.init()

Expand All @@ -75,13 +83,19 @@ public class AdUnit: NSObject, DispatcherDelegate {
}

// Internal only!
convenience init(bidRequester: PBMBidRequesterProtocol, configId: String, size: CGSize?, adFormats: Set<AdFormat>) {
convenience init(
bidRequester: PBMBidRequesterProtocol,
configId: String,
size: CGSize?,
adFormats: Set<AdFormat>
) {
self.init(configId: configId, size: size, adFormats: adFormats)
self.bidRequester = bidRequester
}

deinit {
dispatcher?.invalidate()
impressionTracker.stop()
}

//TODO: dynamic is used by tests
Expand Down Expand Up @@ -116,7 +130,10 @@ public class AdUnit: NSObject, DispatcherDelegate {
/// - Parameters:
/// - adObject: The ad object for which demand is being fetched.
/// - completion: A closure called with the result code indicating the outcome of the demand fetch.
dynamic public func fetchDemand(adObject: AnyObject, completion: @escaping(_ result: ResultCode) -> Void) {
dynamic public func fetchDemand(
adObject: AnyObject,
completion: @escaping(_ result: ResultCode) -> Void
) {
baseFetchDemand(adObject: adObject) { bidInfo in
DispatchQueue.main.async {
completion(bidInfo.resultCode)
Expand All @@ -125,7 +142,10 @@ public class AdUnit: NSObject, DispatcherDelegate {
}

// SDK internal
func baseFetchDemand(adObject: AnyObject? = nil, completion: @escaping (_ bidInfo: BidInfo) -> Void) {
func baseFetchDemand(
adObject: AnyObject? = nil,
completion: @escaping (_ bidInfo: BidInfo) -> Void
) {
if !(self is NativeRequest) {
if adSizes.contains(where: { $0.width < 0 || $0.height < 0 }) {
completion(BidInfo(resultCode: .prebidInvalidSize))
Expand Down Expand Up @@ -180,6 +200,13 @@ public class AdUnit: NSObject, DispatcherDelegate {
return
}

let impressionTrackingPayload = PrebidImpressionTrackerPayload(
cacheID: bidResponse.winningBid?.targetingInfo?["hb_cache_id"],
trackingURLs: bidResponse.winningBid?.impressionTrackingURLs ?? []
)

self.impressionTracker.register(payload: impressionTrackingPayload)

if (!self.timeOutSignalSent) {
let resultCode = self.setUp(adObject, with: bidResponse)
let bidInfo = BidInfo.create(resultCode: resultCode, bidResponse: bidResponse)
Expand Down Expand Up @@ -533,3 +560,4 @@ public class AdUnit: NSObject, DispatcherDelegate {
dispatcher.stop()
}
}

9 changes: 9 additions & 0 deletions PrebidMobile/AdUnits/BannerAdUnit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,13 @@ public class BannerAdUnit: AdUnit, BannerBasedAdUnitProtocol, VideoBasedAdUnitPr

super.adUnitConfig.additionalSizes?.append(contentsOf: sizes)
}

// MARK: Prebid Impression Tracking

/// Sets the view in which Prebid will start tracking an impression.
/// - Parameters:
/// - adView: The ad view that contains ad creative(f.e. GAMBannerView). This object will be used later for tracking `burl`.
public func activatePrebidImpressionTracker(adView: UIView) {
impressionTracker.start(in: adView)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*   Copyright 2018-2024 Prebid.org, Inc.

 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at

 http://www.apache.org/licenses/LICENSE-2.0

 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
 */

import UIKit

/// Schedules the reload tracker to monitor the GAM banner view.
/// Each time the banner view reloads, the viewability tracker is triggered.
class BannerViewImpressionTracker: PrebidImpressionTrackerProtocol {

private weak var monitoredView: UIView?

private var reloadTracker: BannerViewReloadTracker?
private var viewabilityTracker: PBMCreativeViewabilityTracker?

private var payload: PrebidImpressionTrackerPayload?

private var isImpressionTracked = false

private var pollingInterval: TimeInterval {
0.2
}

init() {
reloadTracker = BannerViewReloadTracker(
reloadCheckInterval: pollingInterval
) { [weak self] in
self?.isImpressionTracked = false
self?.attachViewabilityTracker()
}
}

func start(in view: UIView) {
self.monitoredView = view
reloadTracker?.start(in: view)
}

func stop() {
reloadTracker?.stop()
viewabilityTracker?.stop()
}

private func attachViewabilityTracker() {
guard let monitoredView else { return }

viewabilityTracker = PBMCreativeViewabilityTracker(
view: monitoredView,
pollingTimeInterval: pollingInterval,
onExposureChange: { [weak self, weak monitoredView] _, viewExposure in
guard let self = self, let monitoredView else { return }

if viewExposure.exposureFactor > 0 && !self.isImpressionTracked {
self.viewabilityTracker?.stop()
self.isImpressionTracked = true

// Ensure that we found Prebid creative
AdViewUtils.findPrebidCacheID(monitoredView) { [weak self] result in
guard let self = self else { return }

switch result {
case .success(let foundCacheID):
if let trackingURLs = self.payload?.trackingURLs,
let creativeCacheID = self.payload?.cacheID,
foundCacheID == creativeCacheID {
for trackingURL in trackingURLs {
PrebidServerConnection.shared.fireAndForget(trackingURL)
}
}
case .failure(let error):
Log.warn(error.localizedDescription)
}
}
}
}
)

viewabilityTracker?.start()
}

func register(payload: PrebidImpressionTrackerPayload) {
self.payload = payload
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*   Copyright 2018-2024 Prebid.org, Inc.

 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at

 http://www.apache.org/licenses/LICENSE-2.0

 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
 */

import WebKit

/// Schedules an observer for interstitial views and, upon detection, activates the viewability tracker.
class InterstitialImpressionTracker: PrebidImpressionTrackerProtocol {

private var interstitialObserver: InterstitialObserver?
private var viewabilityTracker: PBMCreativeViewabilityTracker?

private var payload: PrebidImpressionTrackerPayload?

private var pollingInterval: TimeInterval {
0.2
}

func start(in view: UIView) {
interstitialObserver = InterstitialObserver(window: view as? UIWindow) { [weak self] view in
self?.attachViewabilityTracker(to: view)
}

interstitialObserver?.start()
}

func stop() {
interstitialObserver?.stop()
viewabilityTracker?.stop()
}

private func attachViewabilityTracker(to view: UIView) {
viewabilityTracker = PBMCreativeViewabilityTracker(
view: view,
pollingTimeInterval: pollingInterval,
onExposureChange: { [weak self, weak view] _, viewExposure in
guard let self, let view else { return }

if viewExposure.exposureFactor > 0 {
self.stop()

// Ensure that we found Prebid creative
AdViewUtils.findPrebidCacheID(view) { result in
switch result {
case .success(let foundCacheID):
if let trackingURLs = self.payload?.trackingURLs,
let creativeCacheID = self.payload?.cacheID,
foundCacheID == creativeCacheID {
for trackingURL in trackingURLs {
PrebidServerConnection.shared.fireAndForget(trackingURL)
}
}
case .failure(let error):
Log.warn(error.localizedDescription)
}
}
}
}
)

viewabilityTracker?.start()
}

func register(payload: PrebidImpressionTrackerPayload) {
self.payload = payload
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*   Copyright 2018-2024 Prebid.org, Inc.

 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at

 http://www.apache.org/licenses/LICENSE-2.0

 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
 */

import UIKit

/// Payload for impression trackers.
struct PrebidImpressionTrackerPayload {

/// seatbid.bid.ext.prebid.targeting.hb_cache_id.
/// Used to identify if SDK found Prebid creative.
let cacheID: String?

/// URL strings to track when impression conditions are met.
let trackingURLs: [String]
}

class PrebidImpressionTracker {

private let tracker: PrebidImpressionTrackerProtocol

init(isInterstitial: Bool) {
if isInterstitial {
tracker = InterstitialImpressionTracker()
} else {
tracker = BannerViewImpressionTracker()
}
}

func register(payload: PrebidImpressionTrackerPayload) {
tracker.register(payload: payload)
}

func start(in adView: UIView) {
DispatchQueue.main.async {
self.tracker.start(in: adView)
}
}

func stop() {
tracker.stop()
}
}
Loading

0 comments on commit 9947afd

Please sign in to comment.