Skip to content

Commit

Permalink
OTT-105: Video Completion Rate Feature (#136)
Browse files Browse the repository at this point in the history
* OTT-130: Injecting Video Tracking Events in VAST  (#130)

* OTT-105_VCR: basic changes

* OTT-105: Changes for injecting Tracking Events

* OTT-130_VCR: Added framework with unit tests for injecting Video Event trackers in VAST

* OTT-130_VCR: Reverted Path replace

* OTT-130: Replaced etree dep with original repository

* OTT-130: Refactored as per latest master

* OTT-130: Refactored

* OTT-130: Refactored

* OTT-130: Reverted unwanted changes

* OTT-130: Added unit tests and refactoring

* OTT-130: InjectVideoEventTrackers will now return error if any

* OTT-130: Refactored

* OTT-130: Removed custom macro support

* OTT-130: Reverted the unwanted changes

* OTT-130: Reverted unwanted changes

* OTT-130: Added Url Query Escape Functionality

* OTT-130: Fixed test failures

* OTT-130: Refactored

* OTT-130: Addressed code review comments from Viral

Co-authored-by: Shriprasad <[email protected]>

* OTT-131: VCR Custom Macro (#132)

* OTT-131: Added support for consuming the custom macros from host company

* OTT-131: Removed unwanted comments

* OTT-131: Addressed code review comments

Co-authored-by: Shriprasad <[email protected]>

* OTT-174: Added bid ID , Ad Unit as tracking parameters and ADomain handling change (#134)

* OTT-174: Added AdUnit parameter inside video event tracker URL

* OTT-174: Added bid id, au tracker parameters. in case of advetiser_id, OW will now send only value at index 0 from bid.ADomain array by extracting the domain
Also handled the CTV/Ad Pod bid.id custom use case

* OTT-174: Ensured bidid value is replaced only in case of OW trackers. (95% reliable)
Also ensured that bidid is not getting added to other trackers, if not present

* OTT-174: Fixed code review comments

Co-authored-by: Shriprasad <[email protected]>

Co-authored-by: Shriprasad <[email protected]>
  • Loading branch information
ShriprasadM and pm-shriprasad-marathe authored Apr 20, 2021
1 parent d0c6926 commit 025b755
Show file tree
Hide file tree
Showing 13 changed files with 1,030 additions and 21 deletions.
3 changes: 2 additions & 1 deletion config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ type Configuration struct {
// RequestValidation specifies the request validation options.
RequestValidation RequestValidation `mapstructure:"request_validation"`
// When true, PBS will assign a randomly generated UUID to req.Source.TID if it is empty
AutoGenSourceTID bool `mapstructure:"auto_gen_source_tid"`
AutoGenSourceTID bool `mapstructure:"auto_gen_source_tid"`
TrackerURL string `mapstructure:"tracker_url"`
}

const MIN_COOKIE_SIZE_BYTES = 500
Expand Down
217 changes: 217 additions & 0 deletions endpoints/events/vtrack.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,27 @@ package events
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"strings"
"time"

"github.com/PubMatic-OpenWrap/openrtb"
accountService "github.com/PubMatic-OpenWrap/prebid-server/account"
"github.com/PubMatic-OpenWrap/prebid-server/adapters"
"github.com/PubMatic-OpenWrap/prebid-server/analytics"
"github.com/PubMatic-OpenWrap/prebid-server/config"
"github.com/PubMatic-OpenWrap/prebid-server/errortypes"
"github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext"
"github.com/PubMatic-OpenWrap/prebid-server/prebid_cache_client"
"github.com/PubMatic-OpenWrap/prebid-server/stored_requests"

// "github.com/beevik/etree"
"github.com/PubMatic-OpenWrap/etree"
"github.com/golang/glog"
"github.com/julienschmidt/httprouter"
)
Expand Down Expand Up @@ -46,6 +53,39 @@ type CacheObject struct {
UUID string `json:"uuid"`
}

// standard VAST macros
// https://interactiveadvertisingbureau.github.io/vast/vast4macros/vast4-macros-latest.html#macro-spec-adcount
const (
VASTAdTypeMacro = "[ADTYPE]"
VASTAppBundleMacro = "[APPBUNDLE]"
VASTDomainMacro = "[DOMAIN]"
VASTPageURLMacro = "[PAGEURL]"

// PBS specific macros
PBSEventIDMacro = "[EVENT_ID]" // macro for injecting PBS defined video event tracker id
//[PBS-ACCOUNT] represents publisher id / account id
PBSAccountMacro = "[PBS-ACCOUNT]"
// [PBS-BIDDER] represents bidder name
PBSBidderMacro = "[PBS-BIDDER]"
// [PBS-BIDID] represents bid id. If auction.generate-bid-id config is on, then resolve with response.seatbid.bid.ext.prebid.bidid. Else replace with response.seatbid.bid.id
PBSBidIDMacro = "[PBS-BIDID]"
// [ADERVERTISER_NAME] represents advertiser name
PBSAdvertiserNameMacro = "[ADVERTISER_NAME]"
// Pass imp.tagId using this macro
PBSAdUnitIDMacro = "[AD_UNIT_ID]"
)

var trackingEvents = []string{"firstQuartile", "midpoint", "thirdQuartile", "complete"}

// PubMatic specific event IDs
// This will go in event-config once PreBid modular design is in place
var eventIDMap = map[string]string{
"firstQuartile": "4",
"midpoint": "3",
"thirdQuartile": "5",
"complete": "6",
}

func NewVTrackEndpoint(cfg *config.Configuration, accounts stored_requests.AccountFetcher, cache prebid_cache_client.Client, bidderInfos adapters.BidderInfos) httprouter.Handle {
vte := &vtrackEndpoint{
Cfg: cfg,
Expand Down Expand Up @@ -302,3 +342,180 @@ func ModifyVastXmlJSON(externalUrl string, data json.RawMessage, bidid, bidder,
}
return json.RawMessage(vast)
}

//InjectVideoEventTrackers injects the video tracking events
//Returns VAST xml contains as first argument. Second argument indicates whether the trackers are injected and last argument indicates if there is any error in injecting the trackers
func InjectVideoEventTrackers(trackerURL, vastXML string, bid *openrtb.Bid, bidder, accountID string, timestamp int64, bidRequest *openrtb.BidRequest) ([]byte, bool, error) {
// parse VAST
doc := etree.NewDocument()
err := doc.ReadFromString(vastXML)
if nil != err {
err = fmt.Errorf("Error parsing VAST XML. '%v'", err.Error())
glog.Errorf(err.Error())
return []byte(vastXML), false, err // false indicates events trackers are not injected
}

//Maintaining BidRequest Impression Map (Copied from exchange.go#applyCategoryMapping)
//TODO: It should be optimized by forming once and reusing
impMap := make(map[string]*openrtb.Imp)
for i := range bidRequest.Imp {
impMap[bidRequest.Imp[i].ID] = &bidRequest.Imp[i]
}

eventURLMap := GetVideoEventTracking(trackerURL, bid, bidder, accountID, timestamp, bidRequest, doc, impMap)
trackersInjected := false
// return if if no tracking URL
if len(eventURLMap) == 0 {
return []byte(vastXML), false, errors.New("Event URLs are not found")
}

creatives := FindCreatives(doc)

if adm := strings.TrimSpace(bid.AdM); adm == "" || strings.HasPrefix(adm, "http") {
// determine which creative type to be created based on linearity
if imp, ok := impMap[bid.ImpID]; ok && nil != imp.Video {
// create creative object
creatives = doc.FindElements("VAST/Ad/Wrapper/Creatives")
// var creative *etree.Element
// if len(creatives) > 0 {
// creative = creatives[0] // consider only first creative
// } else {
creative := doc.CreateElement("Creative")
creatives[0].AddChild(creative)

// }

switch imp.Video.Linearity {
case openrtb.VideoLinearityLinearInStream:
creative.AddChild(doc.CreateElement("Linear"))
case openrtb.VideoLinearityNonLinearOverlay:
creative.AddChild(doc.CreateElement("NonLinearAds"))
default: // create both type of creatives
creative.AddChild(doc.CreateElement("Linear"))
creative.AddChild(doc.CreateElement("NonLinearAds"))
}
creatives = creative.ChildElements() // point to actual cratives
}
}
for _, creative := range creatives {
trackingEvents := creative.SelectElement("TrackingEvents")
if nil == trackingEvents {
trackingEvents = creative.CreateElement("TrackingEvents")
creative.AddChild(trackingEvents)
}
// Inject
for event, url := range eventURLMap {
trackingEle := trackingEvents.CreateElement("Tracking")
trackingEle.CreateAttr("event", event)
trackingEle.SetText(fmt.Sprintf("%s", url))
trackersInjected = true
}
}

out := []byte(vastXML)
var wErr error
if trackersInjected {
out, wErr = doc.WriteToBytes()
trackersInjected = trackersInjected && nil == wErr
if nil != wErr {
glog.Errorf("%v", wErr.Error())
}
}
return out, trackersInjected, wErr
}

// GetVideoEventTracking returns map containing key as event name value as associaed video event tracking URL
// By default PBS will expect [EVENT_ID] macro in trackerURL to inject event information
// [EVENT_ID] will be injected with one of the following values
// firstQuartile, midpoint, thirdQuartile, complete
// If your company can not use [EVENT_ID] and has its own macro. provide config.TrackerMacros implementation
// and ensure that your macro is part of trackerURL configuration
func GetVideoEventTracking(trackerURL string, bid *openrtb.Bid, bidder string, accountId string, timestamp int64, req *openrtb.BidRequest, doc *etree.Document, impMap map[string]*openrtb.Imp) map[string]string {
eventURLMap := make(map[string]string)
if "" == strings.TrimSpace(trackerURL) {
return eventURLMap
}

// lookup custom macros
var customMacroMap map[string]string
if nil != req.Ext {
reqExt := new(openrtb_ext.ExtRequest)
err := json.Unmarshal(req.Ext, &reqExt)
if err == nil {
customMacroMap = reqExt.Prebid.Macros
} else {
glog.Warningf("Error in unmarshling req.Ext.Prebid.Vast: [%s]", err.Error())
}
}

for _, event := range trackingEvents {
eventURL := trackerURL
// lookup in custom macros
if nil != customMacroMap {
for customMacro, value := range customMacroMap {
eventURL = replaceMacro(eventURL, customMacro, value)
}
}
// replace standard macros
eventURL = replaceMacro(eventURL, VASTAdTypeMacro, string(openrtb_ext.BidTypeVideo))
if nil != req && nil != req.App {
// eventURL = replaceMacro(eventURL, VASTAppBundleMacro, req.App.Bundle)
eventURL = replaceMacro(eventURL, VASTDomainMacro, req.App.Bundle)
if nil != req.App.Publisher {
eventURL = replaceMacro(eventURL, PBSAccountMacro, req.App.Publisher.ID)
}
}
if nil != req && nil != req.Site {
eventURL = replaceMacro(eventURL, VASTDomainMacro, req.Site.Domain)
eventURL = replaceMacro(eventURL, VASTPageURLMacro, req.Site.Page)
if nil != req.Site.Publisher {
eventURL = replaceMacro(eventURL, PBSAccountMacro, req.Site.Publisher.ID)
}
}

if len(bid.ADomain) > 0 {
//eventURL = replaceMacro(eventURL, PBSAdvertiserNameMacro, strings.Join(bid.ADomain, ","))
url, err := url.Parse(bid.ADomain[0])
if nil == err {
eventURL = replaceMacro(eventURL, PBSAdvertiserNameMacro, url.Hostname())
} else {
glog.Warningf("Unable to extract domain from '%s'. [%s]", bid.ADomain[0], err.Error())
}
}

eventURL = replaceMacro(eventURL, PBSBidderMacro, bidder)
eventURL = replaceMacro(eventURL, PBSBidIDMacro, bid.ID)
// replace [EVENT_ID] macro with PBS defined event ID
eventURL = replaceMacro(eventURL, PBSEventIDMacro, eventIDMap[event])

if imp, ok := impMap[bid.ImpID]; ok {
eventURL = replaceMacro(eventURL, PBSAdUnitIDMacro, imp.TagID)
}
eventURLMap[event] = eventURL
}
return eventURLMap
}

func replaceMacro(trackerURL, macro, value string) string {
macro = strings.TrimSpace(macro)
if strings.HasPrefix(macro, "[") && strings.HasSuffix(macro, "]") && len(strings.TrimSpace(value)) > 0 {
trackerURL = strings.ReplaceAll(trackerURL, macro, url.QueryEscape(value))
} else {
glog.Warningf("Invalid macro '%v'. Either empty or missing prefix '[' or suffix ']", macro)
}
return trackerURL
}

//FindCreatives finds Linear, NonLinearAds fro InLine and Wrapper Type of creatives
//from input doc - VAST Document
//NOTE: This function is temporarily seperated to reuse in ctv_auction.go. Because, in case of ctv
//we generate bid.id
func FindCreatives(doc *etree.Document) []*etree.Element {
// Find Creatives of Linear and NonLinear Type
// Injecting Tracking Events for Companion is not supported here
creatives := doc.FindElements("VAST/Ad/InLine/Creatives/Creative/Linear")
creatives = append(creatives, doc.FindElements("VAST/Ad/Wrapper/Creatives/Creative/Linear")...)
creatives = append(creatives, doc.FindElements("VAST/Ad/InLine/Creatives/Creative/NonLinearAds")...)
creatives = append(creatives, doc.FindElements("VAST/Ad/Wrapper/Creatives/Creative/NonLinearAds")...)
return creatives
}
Loading

0 comments on commit 025b755

Please sign in to comment.