Skip to content

Commit

Permalink
OTT-58: Extract Duration and Creative Id and update Prebid Server obj…
Browse files Browse the repository at this point in the history
…ects (#95)

* OTT-58: Added empty function getBidDuration. Added unit tests  around it

* OTT-58: Few trials for converting VAST bid duration into seconds when input is  HH:MM:SS.mmm

* OTT-58: Added unit tests and functionality for determining the video ad duration

* OTT-58: Refactored and added some more unit tests

* OTT-58: Added Benchmark testcase

* OTT-58: Removed version argument from tests. Its not required for now. Fixed test issue

* OTT-53: typo fix

* OTT-58: Modifed regexp for millis to accept value max upto 999. Added unit tests around it

* OTT-58: Added handling for detecting creative.id and updating it in typedBid.Bid.CrID

* OTT-58: Addressed code review comments

* OTT-58: Reverted changes for getCreativeID. It will now return empty string to creative id is not present.
Instead caller will generate the random creative id with cr as prefix

Co-authored-by: Shriprasad <[email protected]>
  • Loading branch information
ShriprasadM and pm-shriprasad-marathe authored Jan 4, 2021
1 parent cfa29dc commit b17213c
Show file tree
Hide file tree
Showing 2 changed files with 197 additions and 5 deletions.
82 changes: 80 additions & 2 deletions adapters/tagbidder/vast_tag_response_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@ import (
"fmt"
"math/rand"
"net/http"
"regexp"
"strconv"
"time"

"github.com/PubMatic-OpenWrap/etree"
"github.com/PubMatic-OpenWrap/openrtb"
"github.com/PubMatic-OpenWrap/prebid-server/adapters"
"github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext"
)

var durationRegExp = regexp.MustCompile(`^([01]?\d|2[0-3]):([0-5]?\d):([0-5]?\d)(\.(\d{1,3}))?$`)

//IVASTTagResponseHandler to parse VAST Tag
type IVASTTagResponseHandler interface {
ITagResponseHandler
Expand Down Expand Up @@ -79,8 +83,25 @@ func (handler *VASTTagResponseHandler) vastTagToBidderResponse(internalRequest *
}

typedBid := &adapters.TypedBid{
Bid: &openrtb.Bid{},
BidType: openrtb_ext.BidTypeVideo,
Bid: &openrtb.Bid{},
BidType: openrtb_ext.BidTypeVideo,
BidVideo: &openrtb_ext.ExtBidPrebidVideo{},
}

creatives := adElement.FindElements("Creatives/Creative")
if nil != creatives {
for _, creative := range creatives {
// get creative id
typedBid.Bid.CrID = getCreativeID(creative)
// get duration. Ignore errors
dur, _ := getDuration(creative)
typedBid.BidVideo.Duration = int(dur) // prebid expects int value
}
}

// generate random creative id if not present
if "" == typedBid.Bid.CrID {
typedBid.Bid.CrID = "cr_" + getRandomID()
}

bidResponse := &adapters.BidderResponse{
Expand Down Expand Up @@ -176,3 +197,60 @@ func getCreativeID(ad *etree.Element) string {
var getRandomID = func() string {
return strconv.FormatInt(rand.Int63(), intBase)
}

// getDuration extracts the duration of the bid from input creative of Linear type.
// The lookup may vary from vast version provided in the input
// returns duration in seconds or error if failed to obtained the duration.
// If multple Linear tags are present, onlyfirst one will be used
//
// It will lookup for duration only in case of creative type is Linear.
// If creative type other than Linear then this function will return error
// For Linear Creative it will lookup for Duration attribute.Duration value will be in hh:mm:ss.mmm format as per VAST specifications
// If Duration attribute not present this will return error
//
// After extracing the duration it will convert it into seconds
//
// The ad server uses the <Duration> element to denote
// the intended playback duration for the video or audio component of the ad.
// Time value may be in the format HH:MM:SS.mmm where .mmm indicates milliseconds.
// Providing milliseconds is optional.
//
// Reference
// 1.https://iabtechlab.com/wp-content/uploads/2019/06/VAST_4.2_final_june26.pdf
// 2.https://iabtechlab.com/wp-content/uploads/2018/11/VAST4.1-final-Nov-8-2018.pdf
// 3.https://iabtechlab.com/wp-content/uploads/2016/05/VAST4.0_Updated_April_2016.pdf
// 4.https://iabtechlab.com/wp-content/uploads/2016/04/VASTv3_0.pdf
func getDuration(creative *etree.Element) (float64, error) {
if nil == creative {
return 0, errors.New("Invalid Creative")
}
node := creative.FindElement("./Linear/Duration")
if nil == node {
return 0, errors.New("Invalid Duration")
}
duration := node.Text()
// check if milliseconds is provided
match := durationRegExp.FindStringSubmatch(duration)
if nil == match {
return 0, errors.New("Invalid Duration")
}
repl := "${1}h${2}m${3}s"
ms := match[5]
if "" != ms {
repl += "${5}ms"
}
duration = durationRegExp.ReplaceAllString(duration, repl)
dur, err := time.ParseDuration(duration)
if err != nil {
return 0, err
}
return dur.Seconds(), err
}

//getCreativeID looks for ID inside input creative tag
func getCreativeID(creative *etree.Element) string {
if nil == creative {
return ""
}
return creative.SelectAttrValue("id", "")
}
120 changes: 117 additions & 3 deletions adapters/tagbidder/vast_tag_response_handler_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package tagbidder

import (
"errors"
"testing"

"github.com/PubMatic-OpenWrap/etree"

"github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -32,7 +35,7 @@ func TestVASTTagResponseHandler_vastTagToBidderResponse(t *testing.T) {
internalRequest: &openrtb.BidRequest{
ID: `request_id_1`,
Imp: []openrtb.Imp{
openrtb.Imp{
{
ID: `imp_id_1`,
},
},
Expand All @@ -47,14 +50,16 @@ func TestVASTTagResponseHandler_vastTagToBidderResponse(t *testing.T) {
want: want{
bidderResponse: &adapters.BidderResponse{
Bids: []*adapters.TypedBid{
&adapters.TypedBid{
{
Bid: &openrtb.Bid{
ID: `1234`,
ImpID: `imp_id_1`,
Price: 0.05,
AdM: `<VAST version="2.0"> <Ad id="1"> <InLine> <Creatives> <Creative sequence="1"> <Linear> <MediaFiles> <MediaFile><![CDATA[ad.mp4]]></MediaFile> </MediaFiles> </Linear> </Creative> </Creatives> <Extensions> <Extension type="LR-Pricing"> <Price model="CPM" currency="USD"><![CDATA[0.05]]></Price> </Extension> </Extensions> </InLine> </Ad> </VAST>`,
CrID: "cr_1234",
},
BidType: openrtb_ext.BidTypeVideo,
BidType: openrtb_ext.BidTypeVideo,
BidVideo: &openrtb_ext.ExtBidPrebidVideo{},
},
},
Currency: `USD`,
Expand All @@ -76,3 +81,112 @@ func TestVASTTagResponseHandler_vastTagToBidderResponse(t *testing.T) {
})
}
}

//TestGetDurationInSeconds ...
// hh:mm:ss.mmm => 3:40:43.5 => 3 hours, 40 minutes, 43 seconds and 5 milliseconds
// => 3*60*60 + 40*60 + 43 + 5*0.001 => 10800 + 2400 + 43 + 0.005 => 13243.005
func TestGetDurationInSeconds(t *testing.T) {
type args struct {
creativeTag string // ad element
}
type want struct {
duration float64 // seconds (will converted from string with format as HH:MM:SS.mmm)
durationInt int
err error
}
tests := []struct {
name string
args args
want want
}{
// duration validation tests
{name: "duration 00:00:25 (= 25 seconds)", want: want{duration: 25, durationInt: 25}, args: args{creativeTag: `<Creative sequence="1"> <Linear> <Duration>00:00:25</Duration> </Linear> </Creative>`}},
{name: "duration 00:00:-25 (= -25 seconds)", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: `<Creative sequence="1"> <Linear> <Duration>00:00:-25</Duration> </Linear> </Creative>`}},
{name: "duration 00:00:30.999 (= 30.990 seconds (int -> 30 seconds))", want: want{duration: 30.999, durationInt: 30}, args: args{creativeTag: `<Creative sequence="1"> <Linear> <Duration>00:00:30.999</Duration> </Linear> </Creative>`}},
{name: "duration 00:01:08 (1 min 8 seconds = 68 seconds)", want: want{duration: 68, durationInt: 68}, args: args{creativeTag: `<Creative sequence="1"> <Linear> <Duration>00:01:08</Duration> </Linear> </Creative>`}},
{name: "duration 02:13:12 (2 hrs 13 min 12 seconds) = 7992 seconds)", want: want{duration: 7992, durationInt: 7992}, args: args{creativeTag: `<Creative sequence="1"> <Linear> <Duration>02:13:12</Duration> </Linear> </Creative>`}},
{name: "duration 3:40:43.5 (3 hrs 40 min 43 seconds 5 ms) = 6043.005 seconds (int -> 6043 seconds))", want: want{duration: 13243.005, durationInt: 13243}, args: args{creativeTag: `<Creative sequence="1"> <Linear> <Duration>3:40:43.5</Duration> </Linear> </Creative>`}},
{name: "duration 00:00:25.0005458 (0 hrs 0 min 25 seconds 0005458 ms) - invalid max ms is 999", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: `<Creative sequence="1"> <Linear> <Duration>00:00:25.0005458</Duration> </Linear> </Creative>`}},
{name: "invalid duration 3:13:900 (3 hrs 13 min 900 seconds) = Invalid seconds )", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: `<Creative sequence="1"> <Linear> <Duration>3:13:900</Duration> </Linear> </Creative>`}},
{name: "invalid duration 3:13:34:44 (3 hrs 13 min 34 seconds :44=invalid) = ?? )", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: `<Creative sequence="1"> <Linear> <Duration>3:13:34:44</Duration> </Linear> </Creative>`}},
{name: "duration = 0:0:45.038 , with milliseconds duration (0 hrs 0 min 45 seconds and 038 millseconds) = 45.038 seconds (int -> 45 seconds) )", want: want{duration: 45.038, durationInt: 45}, args: args{creativeTag: `<Creative sequence="1"> <Linear> <Duration>0:0:45.038</Duration> </Linear> </InLine> </Creative>`}},
{name: "duration = 0:0:48.50 = 48.050 seconds (int -> 48 seconds))", want: want{duration: 48.050, durationInt: 48}, args: args{creativeTag: `<Creative sequence="1"> <Linear> <Duration>0:0:48.50</Duration> </Linear> </InLine> </Creative>`}},
{name: "duration = 0:0:28.59 = 28.059 seconds (int -> 28 seconds))", want: want{duration: 28.059, durationInt: 28}, args: args{creativeTag: `<Creative sequence="1"> <Linear> <Duration>0:0:28.59</Duration> </Linear> </InLine> </Creative>`}},
{name: "duration = 56 (ambiguity w.r.t. HH:MM:SS.mmm format)", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: `<Creative sequence="1"> <Linear> <Duration>56</Duration> </Linear> </Creative>`}},
{name: "duration = :56 (ambiguity w.r.t. HH:MM:SS.mmm format)", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: `<Creative sequence="1"> <Linear> <Duration>:56</Duration> </Linear> </Creative>`}},
{name: "duration = :56: (ambiguity w.r.t. HH:MM:SS.mmm format)", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: `<Creative sequence="1"> <Linear> <Duration>:56:</Duration> </Linear> </Creative>`}},
{name: "duration = ::56 (ambiguity w.r.t. HH:MM:SS.mmm format)", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: `<Creative sequence="1"> <Linear> <Duration>::56</Duration> </Linear> </Creative>`}},
{name: "duration = 56.445 (ambiguity w.r.t. HH:MM:SS.mmm format)", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: `<Creative sequence="1"> <Linear> <Duration>56.445</Duration> </Linear> </Creative>`}},
{name: "duration = a:b:c.d (no numbers)", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: `<Creative sequence="1"> <Linear> <Duration>a:b:c.d</Duration> </Linear> </Creative>`}},

// tag validations tests
{name: "Linear Creative no duration", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: `<Creative><Linear><Linear></Creative>`}},
{name: "Companion Creative no duration", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: `<Creative><CompanionAds></CompanionAds></Creative>`}},
{name: "Non-Linear Creative no duration", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: `<Creative><NonLinearAds></NonLinearAds></Creative>`}},
{name: "Invalid Creative tag", want: want{err: errors.New("Invalid Creative")}, args: args{creativeTag: `<Ad></Ad>`}},
{name: "Nil Creative tag", want: want{err: errors.New("Invalid Creative")}, args: args{creativeTag: ""}},

// multiple linear tags in creative
{name: "Multiple Linear Ads within Creative", want: want{duration: 25, durationInt: 25}, args: args{creativeTag: `<Creative><Linear><Duration>0:0:25<Duration></Linear><Linear><Duration>0:0:30<Duration></Linear></Creative>`}},
// Case sensitivity check - passing DURATION (vast is case-sensitive as per https://vastvalidator.iabtechlab.com/dash)
{name: "<DURATION> all caps", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: `<Creative><Linear><DURATION>0:0:10</Duration></Linear></Creative>`}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
doc := etree.NewDocument()
doc.ReadFromString(tt.args.creativeTag)
dur, err := getDuration(doc.FindElement("./Creative"))
assert.Equal(t, tt.want.duration, dur)
assert.Equal(t, tt.want.durationInt, int(dur))
assert.Equal(t, tt.want.err, err)
// if error expects 0 value for duration
if nil != err {
assert.Equal(t, 0.0, dur)
}
})
}
}

func BenchmarkGetDuration(b *testing.B) {
doc := etree.NewDocument()
doc.ReadFromString(`<Creative sequence="1"> <Linear> <Duration>0:0:56.3</Duration> </Linear> </Creative>`)
creative := doc.FindElement("/Creative")
for n := 0; n < b.N; n++ {
getDuration(creative)
}
}

func TestGetCreativeId(t *testing.T) {
type args struct {
creativeTag string // ad element
}
type want struct {
id string
}
tests := []struct {
name string
args args
want want
}{
{name: "creative tag with id", want: want{id: "233ff44"}, args: args{creativeTag: `<Creative id="233ff44"></Creative>`}},
{name: "creative tag without id", want: want{id: ""}, args: args{creativeTag: `<Creative></Creative>`}},
{name: "no creative tag", want: want{id: ""}, args: args{creativeTag: ""}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
doc := etree.NewDocument()
doc.ReadFromString(tt.args.creativeTag)
id := getCreativeID(doc.FindElement("./Creative"))
assert.Equal(t, tt.want.id, id)
})
}
}

func BenchmarkGetCreativeID(b *testing.B) {
doc := etree.NewDocument()
doc.ReadFromString(`<Creative id="132324eerr"> </Creative>`)
creative := doc.FindElement("/Creative")
for n := 0; n < b.N; n++ {
getCreativeID(creative)
}
}

0 comments on commit b17213c

Please sign in to comment.