WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
19 changes: 11 additions & 8 deletions modules/scope3/rtd/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@ hooks:
auth_key: ${SCOPE3_API_KEY} # Set SCOPE3_API_KEY environment variable
endpoint: https://rtdp.scope3.com/prebid/prebid
timeout_ms: 1000
cache_ttl_seconds: 60 # Cache segments for 60 seconds (default)
add_to_targeting: false # Set to true to add segments as individual targeting keys for GAM
masking: # Optional privacy masking configuration
enabled: true # Enable field masking before sending to Scope3
cache_ttl_seconds: 60 # Cache segments for 60 seconds (default)
add_to_targeting: false # Set to true to add segments as individual targeting keys for GAM
add_scope3_targeting_section: false # Also set targeting in dedicated scope3 section
single_segment_key: "" # When set, adds all segments as a comma separated value under a single targeting key
masking: # Optional privacy masking configuration
enabled: true # Enable field masking before sending to Scope3
geo:
preserve_metro: true # Preserve DMA code (default: true)
preserve_zip: true # Preserve postal code (default: true)
Expand All @@ -46,12 +48,12 @@ hooks:
hook_sequence:
- module_code: "scope3.rtd"
hook_impl_code: "HandleEntrypointHook"
raw_auction_request:
auction_processed:
groups:
- timeout: 2000
hook_sequence:
- module_code: "scope3.rtd"
hook_impl_code: "HandleRawAuctionHook"
hook_impl_code: "HandleAuctionProcessedHook"
auction_response:
groups:
- timeout: 5
Expand All @@ -73,6 +75,7 @@ hooks:
"timeout_ms": 1000,
"cache_ttl_seconds": 60,
"add_to_targeting": false,
"add_scope3_targeting_section": false,
"masking": {
"enabled": true,
"geo": {
Expand Down Expand Up @@ -306,15 +309,15 @@ The module forwards any fields that are not masked from the bid request to the S
### Auction Response Data
The module adds audience segments to the auction response, giving publishers full control over how to use them:

1. **Publisher Flexibility**: Segments are always returned in `ext.scope3.segments` for the publisher to decide where to send
1. **Publisher Flexibility**: Segments are returned in `ext.scope3.segments` when configured for the publisher to decide where to send
2. **Google Ad Manager (GAM)**: Individual targeting keys are added when `add_to_targeting: true` (e.g., `gmp_eligible=true`)
3. **Other Ad Servers**: Publisher can forward segments to any ad server or system
4. **Analytics**: Segment data is available for reporting and analysis

### Response Format Options
The module provides segments in two formats:

**Always available:**
**When `add_scope3_targeting_section: true`:**
```json
{
"ext": {
Expand Down
198 changes: 83 additions & 115 deletions modules/scope3/rtd/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ import (
"errors"
"fmt"
"hash"
"maps"
"net/http"
"slices"
"strings"
"sync"
"time"
Expand All @@ -26,6 +24,7 @@ import (
"github.com/prebid/prebid-server/v3/modules/moduledeps"
"github.com/prebid/prebid-server/v3/util/iterutil"
"github.com/prebid/prebid-server/v3/util/jsonutil"
"github.com/tidwall/sjson"
)

// Builder is the entry point for the module
Expand Down Expand Up @@ -90,31 +89,34 @@ func Builder(config json.RawMessage, deps moduledeps.ModuleDeps) (interface{}, e

const (
// keys for miCtx
asyncRequestKey = "scope3.AsyncRequest"
scope3MacroKey = "scope3_macro"
scope3MacroSeparator = ";"
asyncRequestKey = "scope3.AsyncRequest"
scope3MacroKey = "scope3_macro"
scope3IncludeKey = "scope3_include"
scope3Separator = ";"
)

var scope3MacroKeyPlusSeparator = scope3MacroKey + scope3MacroSeparator
var scope3MacroKeyPlusSeparator = scope3MacroKey + scope3Separator
var scope3IncludeKeyPlusSeparator = scope3IncludeKey + scope3Separator

const DefaultScope3RTDURL = "https://rtdp.scope3.com/prebid/prebid"

var (
// Declare hooks
_ hookstage.Entrypoint = (*Module)(nil)
_ hookstage.RawAuctionRequest = (*Module)(nil)
_ hookstage.AuctionResponse = (*Module)(nil)
_ hookstage.Entrypoint = (*Module)(nil)
_ hookstage.ProcessedAuctionRequest = (*Module)(nil)
_ hookstage.AuctionResponse = (*Module)(nil)
)

// Config holds module configuration
type Config struct {
Endpoint string `json:"endpoint"`
AuthKey string `json:"auth_key"`
Timeout int `json:"timeout_ms"`
CacheTTL int `json:"cache_ttl_seconds"` // Cache segments for this many seconds
CacheSize int `json:"cache_size"` // Maximum size of segment cache in bytes
AddToTargeting bool `json:"add_to_targeting"` // Add segments as individual targeting keys
Masking MaskingConfig `json:"masking"` // Privacy masking configuration
Endpoint string `json:"endpoint"`
AuthKey string `json:"auth_key"`
Timeout int `json:"timeout_ms"`
CacheTTL int `json:"cache_ttl_seconds"` // Cache segments for this many seconds
CacheSize int `json:"cache_size"` // Maximum size of segment cache in bytes
AddToTargeting bool `json:"add_to_targeting"` // Add segments as individual targeting keys
AddScope3TargetingSection bool `json:"add_scope3_targeting_section"` // Add segments as individual targeting keys in Scope3 targeting section
Masking MaskingConfig `json:"masking"` // Privacy masking configuration
}

// MaskingConfig controls what user data is masked before sending to Scope3
Expand Down Expand Up @@ -153,30 +155,19 @@ type userExt struct {

// Response types for Scope3 API
type Scope3Response struct {
Data []Scope3Data `json:"data"`
AEESignals `json:"aee_signals"`
}

type Scope3Data struct {
Destination string `json:"destination"`
Imp []Scope3ImpData `json:"imp"`
type AEESignals struct {
Include []string `json:"include,omitempty"`
Exclude []string `json:"exclude,omitempty"`
Macro string `json:"macro,omitempty"` // base64
Bidders []map[string]Bidder `json:"bidders,omitempty"`
}

type Scope3ImpData struct {
ID string `json:"id"`
Ext *Scope3Ext `json:"ext,omitempty"`
}

type Scope3Ext struct {
Scope3 *Scope3ExtData `json:"scope3"`
}

type Scope3ExtData struct {
Segments []Scope3Segment `json:"segments"`
Macro string `json:"macro"`
}

type Scope3Segment struct {
ID string `json:"id"`
type Bidder struct {
Segments []string `json:"segments,omitempty"`
Deals []string `json:"deals,omitempty"`
}

// Module implements the Scope3 RTD module
Expand All @@ -203,13 +194,13 @@ func (m *Module) HandleEntrypointHook(
}

// HandleRawAuctionHook is called early in the auction to fetch Scope3 data
func (m *Module) HandleRawAuctionHook(
func (m *Module) HandleProcessedAuctionHook(
ctx context.Context,
miCtx hookstage.ModuleInvocationContext,
payload hookstage.RawAuctionRequestPayload,
) (hookstage.HookResult[hookstage.RawAuctionRequestPayload], error) {
var ret hookstage.HookResult[hookstage.RawAuctionRequestPayload]
analyticsNamePrefix := "HandleRawAuctionHook."
payload hookstage.ProcessedAuctionRequestPayload,
) (hookstage.HookResult[hookstage.ProcessedAuctionRequestPayload], error) {
var ret hookstage.HookResult[hookstage.ProcessedAuctionRequestPayload]
analyticsNamePrefix := "HandleProcessedAuctionHook."

asyncRequest, ok := miCtx.ModuleContext[asyncRequestKey].(*AsyncRequest)
if !ok {
Expand All @@ -227,25 +218,8 @@ func (m *Module) HandleRawAuctionHook(
return ret, nil
}

// Parse OpenRTB request here rather than HandleProcessedAuctionHook to get a copy to avoid parallel mutation issues
var bidRequest openrtb2.BidRequest
if err := jsonutil.Unmarshal(payload, &bidRequest); err != nil {
// Log error but don't fail the auction
ret.AnalyticsTags = hookanalytics.Analytics{
Activities: []hookanalytics.Activity{{
Name: analyticsNamePrefix + "bidRequest.unmarshal",
Status: hookanalytics.ActivityStatusError,
Results: []hookanalytics.Result{{
Status: hookanalytics.ResultStatusError,
Values: map[string]interface{}{"error": err.Error()},
}},
}},
}
return ret, nil
}

// Start async request to Scope3
asyncRequest.fetchScope3SegmentsAsync(&bidRequest)
asyncRequest.fetchScope3SegmentsAsync(payload.Request.BidRequest)

return ret, nil
}
Expand Down Expand Up @@ -313,50 +287,58 @@ func (m *Module) HandleAuctionResponseHook(
// Add segments to the auction response
ret.ChangeSet.AddMutation(
func(payload hookstage.AuctionResponsePayload) (hookstage.AuctionResponsePayload, error) {
// Add Scope3 segments to the response ext so publisher can use them
if payload.BidResponse.Ext == nil {
payload.BidResponse.Ext = json.RawMessage("{}")
}

var extMap map[string]interface{}
if err = jsonutil.Unmarshal(payload.BidResponse.Ext, &extMap); err != nil {
extMap = make(map[string]interface{})
}

// Add segments as individual targeting keys for GAM integration
if m.cfg.AddToTargeting {
prebidMap, ok := extMap["prebid"].(map[string]interface{})
if !ok {
prebidMap = make(map[string]interface{})
extMap["prebid"] = prebidMap
}
targetingMap, ok := prebidMap["targeting"].(map[string]interface{})
if !ok {
targetingMap = make(map[string]interface{})
prebidMap["targeting"] = targetingMap
}
// Add each segment as individual targeting key
for _, segment := range segments {
if strings.HasPrefix(segment, scope3MacroKeyPlusSeparator) {
macroKeyVal := strings.Split(segment, scope3MacroSeparator)
if len(macroKeyVal) != 2 {
continue
}
targetingMap[macroKeyVal[0]] = macroKeyVal[1]
} else {
targetingMap[segment] = "true"
segmentKeyVal := strings.Split(segment, scope3Separator)
if len(segmentKeyVal) != 2 {
continue
}
newPayload, err := sjson.SetBytes(payload.BidResponse.Ext, "prebid.targeting."+segmentKeyVal[0], segmentKeyVal[1])
if err != nil {
return payload, err
}
payload.BidResponse.Ext = newPayload
}
}

// Always add to a dedicated scope3 section for publisher flexibility
extMap["scope3"] = map[string]interface{}{
"segments": segments,
// Add to a dedicated scope3 section for publisher flexibility when configured
if m.cfg.AddScope3TargetingSection {
newPayload, err := sjson.SetBytes(payload.BidResponse.Ext, "scope3.segments", segments)
if err != nil {
return payload, err
}
payload.BidResponse.Ext = newPayload
}

extResp, err := jsonutil.Marshal(extMap)
if err == nil {
payload.BidResponse.Ext = extResp
// also add to seatbid[].bid[]
for seatBid := range iterutil.SlicePointerValues(payload.BidResponse.SeatBid) {
for bid := range iterutil.SlicePointerValues(seatBid.Bid) {
// Add segments as individual targeting keys for GAM integration
if m.cfg.AddToTargeting {
for _, segment := range segments {
segmentKeyVal := strings.Split(segment, scope3Separator)
if len(segmentKeyVal) != 2 {
continue
}
newPayload, err := sjson.SetBytes(bid.Ext, "prebid.targeting."+segmentKeyVal[0], segmentKeyVal[1])
if err != nil {
return payload, err
}
bid.Ext = newPayload
}
}

// Always add to a dedicated scope3 section for publisher flexibility
if m.cfg.AddScope3TargetingSection {
newPayload, err := sjson.SetBytes(bid.Ext, "scope3.segments", segments)
if err != nil {
return payload, err
}
bid.Ext = newPayload
}
}
}

return payload, nil
Expand All @@ -375,7 +357,7 @@ func (m *Module) fetchScope3Segments(ctx context.Context, bidRequest *openrtb2.B

// Check cache first
if segments, err := m.cache.Get(cacheKey); err == nil {
return strings.Split(string(segments), ","), nil
return strings.Split(string(segments), "|"), nil
}

// Apply privacy masking before sending to Scope3
Expand Down Expand Up @@ -421,31 +403,17 @@ func (m *Module) fetchScope3Segments(ctx context.Context, bidRequest *openrtb2.B
return nil, err
}

// Extract unique segments (exclude destination)
segmentMap := make(map[string]bool)
var macro string
for data := range iterutil.SlicePointerValues(scope3Resp.Data) {
// Extract actual segments from impression-level data
for imp := range iterutil.SlicePointerValues(data.Imp) {
if imp.Ext != nil && imp.Ext.Scope3 != nil {
if imp.Ext.Scope3.Macro != "" {
macro = imp.Ext.Scope3.Macro
}
for segment := range iterutil.SlicePointerValues(imp.Ext.Scope3.Segments) {
segmentMap[segment.ID] = true
}
}
}
segments := []string{}
if scope3Resp.AEESignals.Include != nil && len(scope3Resp.AEESignals.Include) > 0 {
segmentsStr := scope3IncludeKeyPlusSeparator + strings.Join(scope3Resp.AEESignals.Include, ",")
segments = append(segments, segmentsStr)
}

// Convert to slice
segments := slices.AppendSeq(make([]string, 0, len(segmentMap)), maps.Keys(segmentMap))
if macro != "" {
segments = append(segments, scope3MacroKeyPlusSeparator+macro)
if scope3Resp.AEESignals.Macro != "" {
segments = append(segments, scope3MacroKeyPlusSeparator+scope3Resp.AEESignals.Macro)
}

// Cache the result
err = m.cache.Set(cacheKey, []byte(strings.Join(segments, ",")), m.cfg.CacheTTL)
err = m.cache.Set(cacheKey, []byte(strings.Join(segments, "|")), m.cfg.CacheTTL)
if err != nil {
glog.Infof("could not set segments in cache: %v", err)
}
Expand Down
Loading
Loading