Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GPP Phase 3 - Activity control infrastructure #9546

Closed
bretg opened this issue Feb 15, 2023 · 19 comments · Fixed by #9802
Closed

GPP Phase 3 - Activity control infrastructure #9546

bretg opened this issue Feb 15, 2023 · 19 comments · Fixed by #9802
Assignees

Comments

@bretg
Copy link
Collaborator

bretg commented Feb 15, 2023

Splitting up the GPP feature tracked originally by #8991.

Phase 3 is the "Activity Infrastructure". From the document, this phase is described as:

Prebid.js and Prebid Server will define a set of special activities, each of which can be controlled either directly by the publisher or, in the future, via consent modules like GPP.

The activities defined for Prebid.js are in the document section 3.3.3.

The work for this phase includes:

  1. Implement the call to each activity. The interface will need to be specified. It might make sense to do some performance testing for whether activity interfaces should be called once-per-bidder/module or just once and return a list of allowed/disallowed bidders/modules.
  2. Documentation

This issue details a proposal for the Page and ORTB configs that control the activities.

Page Level Config

The condition logic will initially be kept simple, just modulename, which corresponds to biddercode or modulecode. However, the attributes used in conditions could expand in the future to include country, mediatype, regulation scope, or other fields.

Proposed page-level activity controls:

pbjs.setConfig({
  consentManagement: {
    allowActivities: {
      accessDevice: {
        default: false, // for access_device, this is only for vendors
        rules: [{ 
          priority: 1,
          condition: {
            component: { in: ["core"]} // equivalent of strictstorage flag
          },
          allow: true
        }]
      },
      syncUser: {
        default: true,
        rules: [{  // this is an array because conditions could someday be complicated
          priority: 1,
          condition: {
            component: {in: ["bidderA"]}
          },
          allow: false
        }]
      },
      transmit_ufpd: {
        default: false,
        rules: [{
          condition: {
            component: {in: ["bidderB", "module1"]}
          },
          allow: true
        }]
      },
      ...
    }
  }
});

Details

Here's the general form:

allowactivities: {
        ACTIVITY: {
            default: true/false,
            rules: [{
              priority: INTEGER,
              condition: { CONDITION_LOGIC },
              allow: true/false
            },{
              ... more rules ...
            }]
        }
    }

Definitions:

  • default: defines whether to perform this activity if no overrides or rules fire. If default is unspecified, the hardcoded default-default is true.
  • rules: defines one or more conditions. Rules may be prioritized. All rule conditions are processed in priority order for their true/false outcomes. If any rule at a the current priority level matches and resolves to false, the activity will not be allowed. ('overrides' take precedence over 'rules'). Rules processing stops after the first matching priority level.

Rule Priority

  1. The highest priority is 1
  2. There's no defined lowest priority other than MAXINT
  3. The default priority is 10
  4. Group rules by priority
  5. For each priority group, in descending order of priority:
    • If any rule that matches the condition has allow: false, the activity is DENIED.
    • Otherwise, if at least one rule that matches the condition has allow: true, the activity is ALLOWED.
    • If any rule matched, break out of the priority loop.
  6. If none of rules match, and the activity has default: false, the activity is DENIED.
  7. Otherwise, the activity is ALLOWED.

Conditions

In general, a condition is a series of clauses that are processed separately. Only if all clauses present are true is the entire condition true.

condition: {
    CLAUSE1,
    CLAUSE2,
    ...
}

The general forms a condition take:

condition: {
    ATTRIBUTE1: { notin: [array of strings] },
    ATTRIBUTE2: { in: [array of strings] },
    ATTRIBUTE3: { in: string, // processed as { in: [string] }
    ATTRIBUTE4: { notin: string } // processed as { notin: [string] }
}

Requirements:

  1. CLAUSES are ANDed together. i.e. the result of every ATTRIBUTE clause must be true for the entire condition to be true.
  2. It's ok for the same ATTRIBUTE to be present in the condition more than once. Maybe dumb, but ok.
  3. rules may be present in both the request and the account config.

The following attributes may be seen:

  1. component - As needed, PBS should create component names by concatenating TYPE and NAME with a period ('.').
  2. componentName - this is the biddercode, the analytics adapter code, or the module code.
  3. componentType - this is "bidder", "analytics", or for modules: if the module tag contains "rtd", then type is "rtd", otherwise type is "general".
  4. firstPartyComponent - a boolean. The firstPartyComponents are prebid-owned modules: sharedId, currency, etc.

Formal rules of the condition syntax:

  • condition is a conjunction (AND) of expressions, one for each PARAMETER-VALUE pair;
  • VALUE is either {[UNARY_OPERATOR]: VALUE} or {[BINARY_OPERATOR]: LITERAL};
  • LITERAL is anything that's not an object (arrays, strings, numbers, booleans, null);
  • the expression is thus expanded from the leaf upwards as EXP = BINARY_OPERATOR(PVAL, LITERAL), then EXP = UNARY_OPERATOR(EXP) until the root is reached; where PVAL is the parameter's value;
  • (to start with) the only UNARY_OPERATOR is "not"; the only BINARY_OPERATOR is "in";
  • any time a VALUE is required, but an array a is provided instead, substitute a with {in: a}
  • any time a VALUE is required, but a non-array LITERAL v is provided instead, substitute v with {in: [v]}

Request Level Activity Config Example

The use case for request-level override is to support publishers that want slightly different behaviors for different sites across their account.

ORTB-level activity controls are in ext.prebid.allowactivities

{
  consent: {
    allowActivities: {
      transmitEids: { // ok for everyone except bidderA
        default: true,
        rules: [{
          condition: {
            componentName: ["bidderA"]
          },
          allow: false
        }]
      },
      transmitUfpd: {
        default: false,  // let only certain modules get UFPD
        rules: [{
          condition: {
            componentType: ["bidder", "analytics"]
          },
          allow: false
        }],
        rules: [{  // a module injected this logic to request UFPD
          condition: { // but the outcome is that bidderB will be rejected by the override
            componentName: ["bidderB", "vendor.module1"]
          },
          allow: true
        }]
      },
      ...
    }
  }
}

Account-Level Activity Config Example

The proposed account-level config follows the same pattern as the request config.

{
  consent: {
    allowActivities: {
      fetchBid: {   // always allow all bidders
        rules: [{
          condition: {
            componentType: "bidder"
          },
          allow: true
        }]
      },
      transmitGeo: { // only allow named analytics adapters to get precise geo
        default: false,
        rules: [{
          priority: 1,
          condition: {
            componentType: { not: "analytics" }
          },
          allow: false
        },{
          condition: {
            componentName: ["analytics.reporter1"]
          },
          allow: true
        }]
      },
      ...
    }
  }
}

ORTB controls

Prebid Server will support a similar config at both the account-level and the the request level.

See prebid/prebid-server#2591 for the ext.prebid extension proposed.

@dgirardi
Copy link
Collaborator

Is a module name enough, or do we need to pair it with a type? Would I have a reason to block storage for rubicon analytics but allow it for the bid adapter for example?

@bretg
Copy link
Collaborator Author

bretg commented Feb 21, 2023

Will leave that to @patmmccann. I'd like to keep this simple if possible.

@dgirardi
Copy link
Collaborator

To clarify:

  • The first condition listed under overrides that matches decides whether the action is allowed or not
  • If none exists, if any matching condition listed under rules says allow: false, the action is not allowed
  • If none of the conditions match and the action has default: false , the action is not allowed
  • Otherwise, allow.

Also, I'm not sure about using plurals for the condition variables. I would keep them singular and move the semantics of "matches if it's one of" to the array. This makes no difference now, e.g.

condition: {
    modulename: ["bidderA", "bidderB"],
    mediatype: ["video", "banner"]
}

would mean "module name is either bidderA or bidderB, and mediatype is either video or banner". But it seems more amenable for possible future expansions like maybe

condition: {
    modulename: {
         NOT: ["bidderA", "bidderB"]
    }
}

which could mean "all modules except bidderA or bidderB". (granted, we could still use the plural "modulenames", but it would somehow look off to me).

@dgirardi
Copy link
Collaborator

Another point: I think we typically camelCase for setConfig. Maybe we could go with moduleName in JS, and modulename in ORTB? Not sure if it's worth the headache.

@bretg
Copy link
Collaborator Author

bretg commented Feb 21, 2023

  • Right, condition is an "OR-within-AND-between" structure.
  • I agree that we might extend the condition logic in the future to support "not", "and", "or" operators. But I believe we discussed starting simple.
  • I'm fine with modulename being singular. Updated the description
  • If PBJS would like to continue the camelcase thing, I'm ok with that too.

@bretg
Copy link
Collaborator Author

bretg commented Feb 24, 2023

The Prebid Server team met today and agreed to use the term 'component' rather than 'modulename'. This is because on the server side, "modules" are quite different from bid adapters and analytics adapters. The term "component" covers all three.

Also, we discussed the namespace of components. We're proposing a pattern of COMPONENTTYPE.NAME. e.g.

  • module.prebid.ortb_blocking
  • bidder.appnexus
  • analytics.rubicon
    If there's no dot, then it's assumed to be a bidder code.

It's not clear that PBJS needs to be fully in sync with this since bidders should be compatible, but it's worth discussing. Someday the same module could exist on both sides, so perhaps syncing on the namespace is needed.

@patmmccann patmmccann changed the title GPP Phase 3 GPP Phase 3 - Activity control infrastructure Feb 27, 2023
@bretg
Copy link
Collaborator Author

bretg commented Feb 27, 2023

Open items:

  • Is the activity infrastructure core or a module?
  • How are we going to model the functionality as it changes from the current behavior to the new activity infrastructure? What happens with 7.X when we release the activity infra?
  • Should we map the old interface (e.g. strictStorage) to the new interface? If so, someone is going to have to analyze the old interface and define how they map (invisibly) to the activity infra.

@dgirardi dgirardi self-assigned this Feb 27, 2023
@dgirardi
Copy link
Collaborator

dgirardi commented Feb 28, 2023

Default ( initial condition without any config )

All actions are allowed, except for device storage from bid adapters.

missing: a way to express the default for a subset of an activity ("default is DENY only for bidders").
Alternatively, a way to express an exception from the default for the negation of a condition ("default is DENY, but ALLOW everything that's NOT a bidder").

Proposal (NOT syntax)

setConfig({
    allowActivities: {
        accessDevice: {
            default: false,
            rules: [{
                condition: {
                    componentType: {
                        not: "bidder"
                    }
                },
                allow: true
            }]
        }
    }
})

Device storage

bidderSettings.*.storageAllowed

Current control:

bidderSettings.default.storageAllowed = true;

Equivalent rule:

mergeConfig({
    allowActivities: {
        accessDevice: {
            rules: [{
                condition: {
                    componentType: "bidder"
                },
                allow: true
            }]
        }
    }
})

Current control:

bidderSettings.someBidder.storageAllowed = true;

Equivalent rule:

mergeConfig({
    allowActivities: {
        accessDevice: {
            rules: [{
                condition: {
                    component: "bidder.someBidder"
                },
                allow: true
            }]
        }
    }
})

Current (proposed) control:

bidderSettings.someBidder.storageAllowed = ['html5'];

Equivalent rule:

mergeConfig({
    allowActivities: {
        accessDevice: {
            rules: [{
                condition: {
                    component: "bidder.someBidder",
                    storageMethod: ["html5"]
                },
                allow: true
            }]
        }
    }
})

deviceAccess

Current control:

setConfig({
    deviceAccess: false
})

Equivalent rule:

mergeConfig({
    allowActivities: {
        accessDevice: {
            overrides: [{
                allow: false
            }]
        }
    }
})

@dgirardi
Copy link
Collaborator

dgirardi commented Feb 28, 2023

GDPR

consent string translation examples

All purposes denied, enforcePurpose = true

mergeConfig({
    allowActivities: {
        accessDevice: {
            // purpose 1 - device storage
            rules: [{
                condition: {
                    // by default strictStorageEnforcement = false and core / "vendorless" modules are allowed to ignore consent
                    firstPartyComponent: false,
                },
                allow: false
            }]
        },
        invokeComponent: { // NOTE: simplifying `invokeBidder`, `invokeUserid`, etc into this single activity 
            rules: [
                {
                    // purpose 1 - userId
                    condition: {
                        componentType: "userId",
                        firstPartyComponent: false,
                    },
                    allow: false
                },
                {
                    // purpose 2 - participate in auction
                    condition: {
                        condition: {
                            componentType: "bidder",
                        },
                        allow: false
                    }
                },
                {
                    // purpose 7 - analytics
                    condition: {
                        componentType: "analytics"
                    },
                    allow: false
                }
            ]
        },
        syncUser: {
            // purpose 1 - user sync
            rules: [{
                allow: false
            }]
        }
    }
})

Purpose 1 consent denied, enforcePurpose = true, strictStorageEnforcement = true

mergeConfig({
    allowActivities: {
        accessDevice: {
            rules: [{
                allow: false
            }]
        },
        invokeComponent: { 
            rules: [
                {
                    // purpose 1 - userId
                    condition: {
                        componentType: "userId",
                    },
                    allow: false
                },
            ]
        },
        syncUser: {
            // purpose 1 - user sync
            rules: [{
                allow: false
            }]
        }
    }
})

Purpose 1 consent denied, with vendor exceptions

Note that this is a proposal for how the combination of a particular GDPR consent object and consentMangement.gdpr.rules[].vendorExceptions would be translated into allowActivities. In practice, setting a blanket vendor exception would become simpler, see below.

mergeConfig({
    allowActivities: {
        accessDevice: {
            rules: [{
                condition: {
                    componentName: {
                        NOT: [
                            "exceptedVendorA",
                            "exceptedVendorB"
                        ]
                    }
                },
                allow: false
            }]
        },
        invokeComponent: { 
            rules: [
                {
                    condition: {
                        componentType: "userId",
                        componentName: {
                            NOT: [
                                "exceptedVendorA",
                                "exceptedVendorB"
                            ]
                        }
                    },
                    allow: false
                },
            ]
        },
        syncUser: {
            rules: [{
                condition: {
                    componentName: {
                        NOT: [
                            "exceptedVendorA",
                            "exceptedVendorB"
                        ]
                    }
                },
                allow: false,
            }]
        }
    }
})

Purpose 1 consent denied for particular vendors

Note: vendorExceptions and softVendorExceptions are to be subtracted from the set of vendors that were denied consent. E.g., if the CMP indicates that
{"vendorA", "vendorB", "vendorC"} are denied consent for purpose 1; and the consentManagement.gdpr config declares a soft vendor exception for vendorB,
the "true" set of vendors that have no consent is {"vendorA", "vendorC"}.

mergeConfig({
    allowActivities: {
        accessDevice: {
            rules: [{
                componentName: [
                    "vendorA", // determined from reverse-GVLID lookup
                    "vendorC"
                ],
                allow: false
            }]
        },
        invokeComponent: {
            rules: [{
                condition: {
                    component: [
                        "userId.vendorA",
                        "userId.vendorC"
                    ],
                },
                allow: false
            }]
        },
        syncUser: {
            rules: [{
                condition: {
                    componentName: [
                        "bidder.vendorA",
                        "bidder.vendorC"
                    ],
                },
                allow: false
            }]
        }
    }
})

"New-style" vendor exceptions (always allow a particular vendor, no matter the context)

mergeConfig({
    allowActivities: {
       accessDevice: {
          overrides: [{
             condition: {
                  componentName: "vendor"
             },
             allow: true
          }]
       }
    }
})

@dgirardi
Copy link
Collaborator

dgirardi commented Mar 1, 2023

User sync

syncEnabled

Current control

pbjs.setConfig({
   userSync: {
       syncEnabled: false
   } 
});

Equivalent rule:

mergeConfig({
    allowActivities: {
        syncUser: {
            rules: [{
                allow: false
            }]
        }
    }
});

filterSettings

Current control:

pbjs.setConfig({
    userSync: {
        filterSettings: {
            iframe: {
                bidders: ['def'],
                filter: 'exclude'
            },
            image: {
                bidders: ['abc', 'def', 'xyz'],
                filter: 'include'
            }
        }
    }
})

Equivalent rule:

mergeConfig({
    allowActivities: {
        syncUser: {
            rules: [{
                condition: {
                    syncMethod: 'iframe',
                    component: ['bidder.def']
                },
                allow: false
            }, {
                condition: {
                    syncMethod: 'image',
                    component: {
                        NOT: [
                            'bidder.abc',
                            'bidder.def',
                            'bidder.xyz'
                        ]
                    }
                },
                allow: false
            }]
        }
    }
});

@dgirardi
Copy link
Collaborator

dgirardi commented Mar 1, 2023

Summary

  • some more thought to be put into style/capitalization (sync_user, syncUser or syncuser)?
  • the above is an exercise to find what's required to express current controls in the new activity framework. It is not an implementation guide, in practice many optimizations are possible (for example, it likely won't make sense to explicitly "serialize" complicated rules like GDPR strings).
  • all the above controls can continue to work together with other, new activity rules; it looks like no breaking change is required. The only potential point of confusion is the initial default state (a setConfig({allowActivities}) would get rid of it, mergeConfig can keep it, is that clear enough?)

List of requirements (in addition to OP)

  • NOT syntax
  • New universal parameters:
    • componentType and componentName (first and last items in component? what's the use case, if any, for 3+ level namespacing?)
    • firstPartyComponent - true if no 3rd party is involved (currently called vendorless in JS)
  • storageMethod parameter for access_device
  • syncMethod parameter for sync_user

@bretg
Copy link
Collaborator Author

bretg commented Mar 1, 2023

We discussed in PBJS committee, and I appear to be on the losing side of history here, but I'm going to document my concerns as clearly as possible.

There seem to be two approaches with different pros/cons.

Decentralized Activity Controls

In this model, multiple modules can independently submit activity configuration. e.g.

  • GDPR says "activityX is allowed, activityZ is not allowed except for componentA"
  • GPP says "activityX is allowed except for componentB" and says nothing about activityZ
  • HypotheticFutureConsentMechanism says "activityX is not allowed, activityZ is not allowed except for componentB"
  • Publisher-provided Activity control says nothing about activityX but says "activityZ is always allowed"

In this model, we need to define an implicit conflict resolution algorithm for what happens when multiple sources report different results for a given activity.

The proposed algorithm is, I believe:

  1. Process all available activity configs separately to come up with a simple "allowed/not allowed" answer (i.e. resolving any exceptions)
  2. Any direct publisher config always wins. If they don't specify, then the most restrictive (not allowed) answer wins.

Centralized Activity Controls

In this approach, the Activity config is a 'script' that clearly defines how processing takes place.

  • ActivityX: call GPP and HFCM for their consent resolution, but always allow componentA
  • ActivityZ: don't bother calling GDPR, GPP, or HFCM. It's always allowed.

Tradeoffs

In both cases, config is either required or available for all of the systems.

The main differences are, I believe:

  • ability to expand to more sources: the decentralized model is better for this because you can just inject more activity config into the fray without enhancing any other code.
  • readability/debugging: the centralized model is, I believe, better for a human to read and see what factors weigh into a consent decision. The decentralized model will require a skilled debugger and probably changes to the Professor Prebid extension.
  • implementation and performance: I give a slight edge here to the centralized approach. Generating, storing, and processing multiple activity configs consumes CPU. Especially in the case where the publisher is going to throw away whatever results they generate. It might be possible to optimize the components to peek at Activity config and avoid processing, but that adds complexity.

I will now stand down, and will begin to think about how a nightmare world split between legacy TCF and GPP would look from a Prebid Server perspective.

@dgirardi
Copy link
Collaborator

dgirardi commented Mar 1, 2023

Because this was discussed mostly offline; the point of contention is an explicit callout to GPP from activity rules.

Taking this as the example:

ActivityX: call GPP and HFCM for their consent resolution, but always allow componentA
ActivityZ: don't bother calling GDPR, GPP, or HFCM. It's always allowed.

The "centralized" model:

pbjs.setConfig({
     allowActivities: {
          activityX: {
                useGpp: true, // "call GPP for their consent resolution,"
                overrides: [{
                     // "but always allow componentA"
                     condition: {
                          component: "componentA"
                     },
                     allow: true,
                }]
          },
          activityZ: {
             useGpp: false, // "don't bother calling GPP"
             overrides: [{ 
                  allow: true // "it's always allowed"
             }]
          }
     }
})

versus the "decentralized":

pbjs.setConfig({
     allowActivities: {
          activityX: {
                overrides: [{
                     // "always allow componentA" (because I know that GPP may otherwise set its own rule to deny it)
                     condition: {
                          component: "componentA"
                     },
                     allow: true,
                }]
          },
          activityZ: {
             overrides: [{ 
                  allow: true // "it's always allowed"
             }]
          }
     }
})

My retort is that

  • if useGpp is the only flag of this kind, the two forms are equivalent in every way except for the syntax. The only point I can concede is readability.
  • if there is more than one "hidden" policy maker; and therefore more than one flag that we want to make them explicit, then there is a difference; the "decentralized" model has no way to express:
    setConfig({
          allowActivities: {
               activityX: {
                    useGdpr: true  // let GDPR set rules for activityX
                    useUsp: false // but disregard USP
               }
          }
    })
    

In JS this could be done through configuration for the GDPR or USP (sub)modules; but that has the disadvantage that it won't necessarily be as easy to transfer to Prebid Server.

@bretg
Copy link
Collaborator Author

bretg commented Mar 2, 2023

Let's see if we can make some progress on the open items:

componentType and componentName (first and last items in component? what's the use case, if any, for 3+ level namespacing?)

For modules, the PBS committee discussed 3 levels. e.g. module.prebid.ortb-blocking. The middle level is a vendor to prevent conflicts should different vendors supply similar modules. e.g. module.vendorA.safe-creatives and module.vendorB.safe-creatives

some more thought to be put into style/capitalization (sync_user, syncUser or syncuser)?

PBS has a rule where the ORTB can't be camelCased. Some of these proposed words are quite long and are little harder to read without underscores. e.g. invoke_component is easier to read than invokecomponent.

But overall, I don't care so much about this detail. If we want to settle on just camelCase in PBJS that gets translated to lowercase with no underscores for ORTB, ok.

NOT syntax

Here, again, PBS would prefer lowercase and consistency between the positive and negative scenarios. How about:

condition: {
    componentname: {
         notin: ["bidderA", "bidderB"]
    }
}

and

condition: {
    componentname: {
         in: ["bidderA", "bidderB"]
    }
}

I suspect that several of these additional requirements (e.g. firstPartyComponent) aren't relevant for PBS, but it will be fine for PBJS to send them -- they get ignored as needed.

@dgirardi
Copy link
Collaborator

dgirardi commented Mar 2, 2023

For modules, the PBS committee discussed 3 levels. e.g. module.prebid.ortb-blocking.

this seems to confirm that the first item is componentType, last one is componentName; as long as bidder.vendorX and analytics.vendorX are valid full names for bidders / analytics.

e.g. invoke_component is easier to read than invokecomponent.

Can we have consistency? I also don't mind which way this goes, but I see a problem in mixing invoke_component with componentname. That would prevent the camelcase conversion.

Here, again, PBS would prefer lowercase and consistency between the positive and negative scenario

My preference is

condition: {
    component: {
       in: ["first", "second"]
    }
}

and its negation then being

condition: {
    component: {
       not: {
          in: ["first", "second"]
       }
    }
}

because a NOT operator is much more useful than a NOT IN operator. I would not require this level of verbosity though (at least for JS; I won't judge if you prefer it for the wire).

@dgirardi
Copy link
Collaborator

dgirardi commented Mar 2, 2023

To formalize it a bit more:

  • condition is a conjunction (AND) of expressions, one for each PARAMETER-VALUE pair;
  • VALUE is either {[UNARY_OPERATOR]: VALUE} or {[BINARY_OPERATOR]: LITERAL};
  • LITERAL is anything that's not an object (arrays, strings, numbers, booleans, null);
  • the expression is thus expanded from the leaf upwards as EXP = BINARY_OPERATOR(PVAL, LITERAL), then EXP = UNARY_OPERATOR(EXP) until the root is reached; where PVAL is the parameter's value;
  • (to start with) the only UNARY_OPERATOR is "not"; the only BINARY_OPERATOR is "in";

Example:

{
   "condition": {
      "component": {
         "not": {
            "not": {
               "in": [
                  "bidder.vendorA",
                  "bidder.vendorB"
               ] 
            }
         }
      }
   }
}

means the value of the "component" parameter is NOT ( NOT ( ONE OF: "bidder.vendorA", "bidder.vendorB") )

Less contrived example of how this could be expanded in the future:

{
   "condition": {
      "component": {
         "not": {
            "matches": "bidder.*"
         }
      }
   }
}

meaning "component does not match pattern bidder.*"

I would also add the following shorthands (for JS only?)

  • any time a VALUE is required, but an array a is provided instead, substitute a with {in: a}
  • any time a VALUE is required, but a non-array LITERAL v is provided instead, substitute v with {in: [v]}

Examples:

{
    "condition": {
        "component": ["bidder.vendorA", "bidder.vendorB"],
   }
}

translates to

{
    "condition": {
        "component": {
           "in": ["bidder.vendorA", "bidder.vendorB"],
        }
   }
}

and

{
   "condition": {
      "component": {
         "not": "bidder.vendorA"
      }
   }
}

translates to

{
   "condition": {
      "component": {
         "not": {
            "in": ["bidder.vendorA"]
         }
      }
   }
}

@dgirardi
Copy link
Collaborator

dgirardi commented Mar 2, 2023

We reached an agreement on capitalization; JS wil take camelCase for activity and parameter names, but convert them to snake_case if they need to be sent over ORTB. I updated the examples above to use camel case.

Still open:

  • should we have multiple invoke activities (invokeBidder, invokeRtd, etc) or a single invokeComponent?
  • flesh out the component naming convention, especially for modules that do not fit neatly under one of "bidder", "analytics", "userId" or "rtd". For example: floors, currency. (If component always has a vendor in it, e.g generic.prebid.floors, then componentVendor: "prebid" may be more readable than firstPartyComponent: true).

@bretg
Copy link
Collaborator Author

bretg commented Mar 3, 2023

should we have multiple invoke activities (invokeBidder, invokeRtd, etc) or a single invokeComponent?

I vote for separate activities. Several reasons:

  • it's more consistent to have them separate activities
  • if we combine "invoke", shouldn't we consider combining "transmit"? Then we have some activities that are in groups, some that aren't.
  • the rules might be quite different for the different component invocations

Based on your comment in the doc, I made a couple of updates. Primarily, I agree that invoke_rtd was too broad brushed. The main sensitivity about RTD modules sensitive are those that enrich_ufpd. We need to make sure that RTD modules phoning home are subject to the transmit activities.

flesh out the component naming convention

Yeah, it will be awkward if we get this wrong. We already have a "code", which exists pretty much everywhere:

  • For RTD modules, this is called SUBMODULE_NAME. e.g. SirdataRTDModule, blueconic, weborama, RelevadRTDModule
  • For UserID modules, this is called MODULE_NAME. e.g. zeotapIdPlus, pubProvidedId, uid2
  • All analytics adapters have a code
  • All bid adapters have a biddercode

So seems like the least disruptive approach would be to simply concatenate the type (bidder, analytics, rtd, userid, general) with the single existing code. If we wanted to include "vendor", we would have to retrofit 438 files to break that out.

So I'm leaning towards a two-level taxonomy

  • componentType: "bidder", "analytics", "rtd", "userid", "general"
  • componentName: "rubicon", "SirdataRTDModule", "uid2", etc. Names have to be unique within the type.
  • component: the concatenation of TYPE.NAME

Further, I would suggest that component names are allowed to contain a vendor as desired. e.g. PBS has defined "prebid.ortb-blocking" as a name, which is ok. It's just further namespacing within that type.

At least for now, I don't think we need any rule-targeting functionality beyond componentType and componentName. Maybe someday a matches operator would prove useful, but that can wait.

@dgirardi
Copy link
Collaborator

There is no longer a requirement that activity controls should be serializable and understood by Prebid Server. Because of this, the condition syntax can be made more flexible, taking advantage of Javascript.

For example, this:

setConfig({
    allowActivities: {
      fetchBid: {   // always allow all bidders
        rules: [{
          condition: {
            componentType: "bidder"
          },
          allow: true
        }]
      },
      transmitGeo: { // only allow named analytics adapters to get precise geo
        default: false,
        rules: [{
          priority: 1,
          condition: {
            componentType: { not: "analytics" }
          },
          allow: false
        },{
          condition: {
            componentName: ["analytics.reporter1"]
          },
          allow: true
        }]
      },
      ...
    }
})

could become this:

setConfig({
    allowActivities: {
      fetchBid: {   // always allow all bidders
        rules: [{
          condition: function({componentType}) {
                  return componentType === 'bidder'
          },
          allow: true,
        }]
      },
      transmitGeo: { // only allow named analytics adapters to get precise geo
        default: false,
        rules: [{
          priority: 1,
          condition: function({componentType}) { 
               return componentType !== 'analytics'
          },
          allow: false,
        },{
          condition: function({componentName}) {
               return componentName === "analytics.reporter1"
          },
          allow: true,
        }]
      },
      ...
    }
})

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Development

Successfully merging a pull request may close this issue.

2 participants