Skip to content
This repository has been archived by the owner on May 10, 2024. It is now read-only.

[ads] Trigger search result ad viewed event #8758

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,7 @@ var braveTarget: PackageDescription.Target = .target(
.copy("Frontend/Reader/Reader.html"),
.copy("Frontend/Reader/ReaderViewLoading.html"),
.copy("Frontend/Browser/New Tab Page/Backgrounds/Assets/NTP_Images/corwin-prescott-3.jpg"),
.copy("Frontend/UserContent/UserScripts/Scripts_Dynamic/Scripts/DomainSpecific/Paged/BraveSearchResultAdScript.js"),
.copy("Frontend/UserContent/UserScripts/Scripts_Dynamic/Scripts/DomainSpecific/Paged/BraveSearchScript.js"),
.copy("Frontend/UserContent/UserScripts/Scripts_Dynamic/Scripts/DomainSpecific/Paged/BraveSkusScript.js"),
.copy("Frontend/UserContent/UserScripts/Scripts_Dynamic/Scripts/DomainSpecific/Paged/nacl.min.js"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,12 @@ extension BrowserViewController: WKNavigationDelegate {
// The tracker protection script
// This script will track what is blocked and increase stats
.trackerProtectionStats: requestURL.isWebPage(includeDataURIs: false) &&
domainForMainFrame.isShieldExpected(.AdblockAndTp, considerAllShieldsOption: true)
domainForMainFrame.isShieldExpected(.AdblockAndTp, considerAllShieldsOption: true),

// Add Brave search result ads processing script.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Would be great to change this comment like the above comment, i.e., // This script will ...

.searchResultAd: BraveSearchManager.isValidURL(requestURL) &&
!isPrivateBrowsing &&
!rewards.isEnabled
])
}

Expand All @@ -324,6 +329,8 @@ extension BrowserViewController: WKNavigationDelegate {
return (.cancel, preferences)
}

tab?.braveSearchResultAdManager = BraveSearchResultAdManager(url: requestURL, rewards: rewards, isPrivateBrowsing: isPrivateBrowsing)
Copy link
Collaborator

Choose a reason for hiding this comment

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

What if tab?.loadRequest(modifiedRequest) is called on line 328. I know that BraveSearchResultAdManager returns nil for Brave Rewards users, however it may not be clear to others in the future if/when these events are sent when ads are enabled.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

if tab?.loadRequest(modifiedRequest) is called, then we will run this code again but with X-Brave-Ads-Enabled header. tab?.braveSearchResultAdManager will be nil because rewards are enabled. Seems like this works as expected.


// We fetch cookies to determine if backup search was enabled on the website.
let profile = self.profile
let cookies = await webView.configuration.websiteDataStore.httpCookieStore.allCookies()
Expand All @@ -347,6 +354,7 @@ extension BrowserViewController: WKNavigationDelegate {
}
}
} else {
tab?.braveSearchResultAdManager = nil
tab?.braveSearchManager = nil
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2554,7 +2554,8 @@ extension BrowserViewController: TabDelegate {
injectedScripts += [
LoginsScriptHandler(tab: tab, profile: profile, passwordAPI: braveCore.passwordAPI),
EthereumProviderScriptHandler(tab: tab),
SolanaProviderScriptHandler(tab: tab)
SolanaProviderScriptHandler(tab: tab),
BraveSearchResultAdScriptHandler(tab: tab)
]
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright 2024 The Brave Authors. All rights reserved.
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

import BraveCore

// A helper class to handle Brave Search Result Ads.
class BraveSearchResultAdManager: NSObject {
private let rewards: BraveRewards

init?(url: URL, rewards: BraveRewards, isPrivateBrowsing: Bool) {
if !BraveSearchManager.isValidURL(url) ||
isPrivateBrowsing ||
rewards.isEnabled {
return nil
}

self.rewards = rewards
}

func triggerSearchResultAdEvent(_ searchResultAdInfo: BraveAds.SearchResultAdInfo) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: triggerSearchResultAdViewedEvent

Copy link
Collaborator

Choose a reason for hiding this comment

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

.clicked event is missing

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

renamed to triggerSearchResultAdViewedEvent. Clicked event support will be added as a follow-up

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ah yes because we will not enable the feature via Griffin until click support is added. Perfect!

rewards.ads.triggerSearchResultAdEvent(

Check failure on line 23 in Sources/Brave/Frontend/Browser/Search/BraveSearchResultAdManager.swift

View workflow job for this annotation

GitHub Actions / Run tests

value of type 'BraveAds' has no member 'triggerSearchResultAdEvent'

Check failure on line 23 in Sources/Brave/Frontend/Browser/Search/BraveSearchResultAdManager.swift

View workflow job for this annotation

GitHub Actions / Run tests

value of type 'BraveAds' has no member 'triggerSearchResultAdEvent'
searchResultAdInfo,
eventType: .viewed,

Check failure on line 25 in Sources/Brave/Frontend/Browser/Search/BraveSearchResultAdManager.swift

View workflow job for this annotation

GitHub Actions / Run tests

cannot infer contextual base in reference to member 'viewed'

Check failure on line 25 in Sources/Brave/Frontend/Browser/Search/BraveSearchResultAdManager.swift

View workflow job for this annotation

GitHub Actions / Run tests

cannot infer contextual base in reference to member 'viewed'
completion: { _ in })
}
}
3 changes: 3 additions & 0 deletions Sources/Brave/Frontend/Browser/Tab.swift
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,9 @@ class Tab: NSObject {
/// A helper property that handles native to Brave Search communication.
var braveSearchManager: BraveSearchManager?

/// A helper property that handles Brave Search Result Ads.
var braveSearchResultAdManager: BraveSearchResultAdManager?

private lazy var refreshControl = UIRefreshControl().then {
$0.addTarget(self, action: #selector(reload), for: .valueChanged)
}
Expand Down
4 changes: 3 additions & 1 deletion Sources/Brave/Frontend/Browser/UserScriptManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ class UserScriptManager {
case readyStateHelper
case ethereumProvider
case solanaProvider
case searchResultAd
case youtubeQuality

fileprivate var script: WKUserScript? {
Expand All @@ -118,7 +119,8 @@ class UserScriptManager {
case .trackerProtectionStats: return ContentBlockerHelper.userScript
case .ethereumProvider: return EthereumProviderScriptHandler.userScript
case .solanaProvider: return SolanaProviderScriptHandler.userScript

case .searchResultAd: return BraveSearchResultAdScriptHandler.userScript

// Always enabled scripts
case .faviconFetcher: return FaviconScriptHandler.userScript
case .rewardsReporting: return RewardsReportingScriptHandler.userScript
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// Copyright 2024 The Brave Authors. All rights reserved.
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

import WebKit
import BraveCore
import os.log

class BraveSearchResultAdScriptHandler: TabContentScript {
private struct SearchResultAdResponse: Decodable {
struct SearchResultAd: Decodable {
let creativeInstanceId: String
let placementId: String
let creativeSetId: String
let campaignId: String
let advertiserId: String
let landingPage: URL
let headlineText: String
let description: String
let rewardsValue: String
let conversionUrlPatternValue: String?
let conversionAdvertiserPublicKeyValue: String?
let conversionObservationWindowValue: Int?
}

let creatives: [SearchResultAd]
}

fileprivate weak var tab: Tab?

init(tab: Tab) {
self.tab = tab
}

static let scriptName = "BraveSearchResultAdScript"
static let scriptId = UUID().uuidString
static let messageHandlerName = "\(scriptName)_\(messageUUID)"
static let scriptSandbox: WKContentWorld = .page
static let userScript: WKUserScript? = {
guard var script = loadUserScript(named: scriptName) else {
return nil
}
return WKUserScript(source: secureScript(handlerName: messageHandlerName,
securityToken: scriptId,
script: script),
injectionTime: .atDocumentEnd,
forMainFrameOnly: true,
in: scriptSandbox)
}()

func userContentController(
_ userContentController: WKUserContentController,
didReceiveScriptMessage message: WKScriptMessage,
replyHandler: (Any?, String?) -> Void
) {
defer { replyHandler(nil, nil) }

if !verifyMessage(message: message) {
assertionFailure("Missing required security token.")
return
}

guard let tab = tab,
let braveSearchResultAdManager = tab.braveSearchResultAdManager
else {
Logger.module.error("Failed to get Brave search result ad handler")
return
}

guard JSONSerialization.isValidJSONObject(message.body),
let messageData = try? JSONSerialization.data(withJSONObject: message.body, options: []),
let searchResultAds = try? JSONDecoder().decode(SearchResultAdResponse.self, from: messageData)
else {
Logger.module.error("Failed to process Brave search result ads")
Copy link
Collaborator

@tmancey tmancey Feb 9, 2024

Choose a reason for hiding this comment

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

Maybe Failed to pars search result ads response

return
}

processSearchResultAds(searchResultAds, braveSearchResultAdManager: braveSearchResultAdManager)
}

private func processSearchResultAds(
_ searchResultAds: SearchResultAdResponse,
braveSearchResultAdManager: BraveSearchResultAdManager
) {
for ad in searchResultAds.creatives {
guard let rewardsValue = Double(ad.rewardsValue)
else {
Logger.module.error("Failed to process Brave search result ads")
Copy link
Collaborator

Choose a reason for hiding this comment

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

ad not ads as this failure appears to be on a single ad

Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe Failed to process search result ads JSON-LD

return
}

var conversionInfo: BraveAds.ConversionInfo?
Copy link
Collaborator

@tmancey tmancey Feb 9, 2024

Choose a reason for hiding this comment

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

nit: var conversion: BraveAds.ConversionInfo

if let conversionUrlPatternValue = ad.conversionUrlPatternValue,
let conversionObservationWindowValue = ad.conversionObservationWindowValue {
let timeInterval = TimeInterval(conversionObservationWindowValue * 24 * 60 * 60)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe define 24 * 60 * 60 as a constant

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

added private static secondsInDay = 24 * 60 * 60;

conversionInfo = .init(
urlPattern: conversionUrlPatternValue,
verifiableAdvertiserPublicKeyBase64: ad.conversionAdvertiserPublicKeyValue,
observationWindow: Date(timeIntervalSince1970: timeInterval)
)
}

let searchResultAdInfo: BraveAds.SearchResultAdInfo = .init(
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: let searchResultAd...

type: .searchResultAd,
placementId: ad.placementId,
creativeInstanceId: ad.creativeInstanceId,
creativeSetId: ad.creativeSetId,
campaignId: ad.campaignId,
advertiserId: ad.advertiserId,
targetUrl: ad.landingPage,
headlineText: ad.headlineText,
description: ad.description,
value: rewardsValue,
conversion: conversionInfo
)

braveSearchResultAdManager.triggerSearchResultAdEvent(searchResultAdInfo)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright 2024 The Brave Authors. All rights reserved.
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

'use strict';

window.__firefox__.includeOnce('BraveSearchResultAdScript', function($) {
let sendMessage = $(function(creatives) {
$.postNativeMessage('$<message_handler>', {
"securityToken": SECURITY_TOKEN,
"creatives": creatives
});
});

let getJsonLdCreatives = () => {
const scripts = document.querySelectorAll('script[type="application/ld+json"]');
const jsonLdList = Array.from(scripts).map(script => JSON.parse(script.textContent));

if (!jsonLdList) {
return [];
}

const creativeFieldNamesMapping = {
'data-creative-instance-id': 'creativeInstanceId',
'data-placement-id': 'placementId',
'data-creative-set-id': 'creativeSetId',
'data-campaign-id': 'campaignId',
'data-advertiser-id': 'advertiserId',
'data-landing-page': 'landingPage',
'data-headline-text': 'headlineText',
'data-description': 'description',
'data-rewards-value': 'rewardsValue',
'data-conversion-url-pattern-value': 'conversionUrlPatternValue',
'data-conversion-advertiser-public-key-value': 'conversionAdvertiserPublicKeyValue',
'data-conversion-observation-window-value': 'conversionObservationWindowValue'
};

let jsonLdCreatives = [];
jsonLdList.forEach(jsonLd => {
if (jsonLd['@type'] === 'Product' && jsonLd.creatives) {
jsonLd.creatives.forEach(creative => {
if (creative['@type'] === 'SearchResultAd') {
let jsonLdCreative = {};
for (let key in creative) {
if (creativeFieldNamesMapping[key]) {
jsonLdCreative[creativeFieldNamesMapping[key]] = creative[key];
}
}
jsonLdCreatives.push(jsonLdCreative);
}
});
}
});

return jsonLdCreatives;
};

sendMessage(getJsonLdCreatives());
});
Loading