Skip to content

Commit

Permalink
Introduce bidding worklets
Browse files Browse the repository at this point in the history
This is the minimum working implementation. Features to be added later include:
- Arbitrary metadata instead of just a static price (google#147)
- trustedBiddingSignalsKeys (google#148)
- auctionSignals (google#149)
- browserSignals (google#150)
- Timeouts (google#151)
- Deduplication of script fetches (google#152)
- Optimization of worker support code (google#153)
- Public TypeScript typings for worklet script authors (google#154)

Fixes: google#20
  • Loading branch information
taymonbeal committed Jun 4, 2021
1 parent 33b932c commit 8358574
Show file tree
Hide file tree
Showing 17 changed files with 1,936 additions and 184 deletions.
106 changes: 83 additions & 23 deletions frame/auction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@

/** @fileoverview Selection of ads, and creation of tokens to display them. */

import { AuctionAd, AuctionAdConfig } from "../lib/shared/api_types";
import { AuctionAdConfig } from "../lib/shared/api_types";
import { isKeyValueObject } from "../lib/shared/guards";
import { logWarning } from "./console";
import { forEachInterestGroup } from "./db_schema";
import { FetchJsonStatus, tryFetchJson } from "./fetch";
import { FetchStatus, tryFetchJavaScript, tryFetchJson } from "./fetch";
import { BidData, CanonicalInterestGroup } from "./types";
import { runBiddingScript } from "./worklet";

/**
* Selects the currently stored ad with the highest price, mints a token that
Expand All @@ -35,29 +37,43 @@ import { FetchJsonStatus, tryFetchJson } from "./fetch";
*
* @param hostname The hostname of the page where the FLEDGE Shim API is
* running.
* @param extraScript Additional JavaScript code to run in the Web Worker before
* anything else. Used only in tests to stub out `console.warn`.
*/
export async function runAdAuction(
{ trustedScoringSignalsUrl }: AuctionAdConfig,
hostname: string
hostname: string,
extraScript = ""
): Promise<string | boolean> {
let winner: AuctionAd | undefined;
const trustedBiddingSignalsUrls = new Set<string>();
const renderUrls = new Set<string>();
const bidPromises: Array<Promise<BidData | null>> = [];
if (
!(await forEachInterestGroup(({ trustedBiddingSignalsUrl, ads }) => {
if (trustedBiddingSignalsUrl !== undefined && ads.length) {
trustedBiddingSignalsUrls.add(trustedBiddingSignalsUrl);
!(await forEachInterestGroup((group) => {
if (!(group.biddingLogicUrl && group.ads.length)) {
return;
}
for (const ad of ads) {
renderUrls.add(ad.renderUrl);
if (!winner || ad.metadata.price > winner.metadata.price) {
winner = ad;
}
if (group.trustedBiddingSignalsUrl !== undefined) {
trustedBiddingSignalsUrls.add(group.trustedBiddingSignalsUrl);
}
bidPromises.push(
fetchScriptAndBid(group.biddingLogicUrl, group, extraScript)
);
}))
) {
return false;
}
let winner;
const renderUrls = new Set<string>();
for (const bidPromise of bidPromises) {
const bidData = await bidPromise;
if (!bidData) {
continue;
}
renderUrls.add(bidData.render);
if (!winner || bidData.bid > winner.bid) {
winner = bidData;
}
}
if (!winner) {
return true;
}
Expand All @@ -79,16 +95,52 @@ export async function runAdAuction(
})()
);
const token = randomToken();
sessionStorage.setItem(token, winner.renderUrl);
sessionStorage.setItem(token, winner.render);
return token;
}

function randomToken() {
return Array.prototype.map
.call(crypto.getRandomValues(new Uint8Array(16)), (byte: number) =>
byte.toString(/* radix= */ 16).padStart(2, "0")
)
.join("");
async function fetchScriptAndBid(
biddingLogicUrl: string,
group: CanonicalInterestGroup,
extraScript: string
) {
const fetchResult = await tryFetchJavaScript(biddingLogicUrl);
switch (fetchResult.status) {
case FetchStatus.OK: {
const bidData = await runBiddingScript(
fetchResult.value,
group,
extraScript
);
if (!bidData) {
// Error has already been logged in the Web Worker.
return null;
}
if (!group.ads.some(({ renderUrl }) => renderUrl === bidData.render)) {
logWarning("Bid render URL", [
bidData.render,
"is not in interest group:",
group,
]);
return null;
}
// Silently discard zero, negative, infinite, and NaN bids.
if (!(bidData.bid > 0 && bidData.bid < Infinity)) {
return null;
}
return bidData;
}
case FetchStatus.NETWORK_ERROR:
// Browser will have logged the error; no need to log it again.
return null;
case FetchStatus.VALIDATION_ERROR:
logWarning("Cannot use bidding script from", [
biddingLogicUrl,
": " + fetchResult.errorMessage,
...(fetchResult.errorData ?? []),
]);
return null;
}
}

async function fetchAndValidateTrustedSignals(
Expand All @@ -115,7 +167,7 @@ async function fetchAndValidateTrustedSignals(
const response = await tryFetchJson(url.href);
const basicErrorMessage = "Cannot use trusted scoring signals from";
switch (response.status) {
case FetchJsonStatus.OK: {
case FetchStatus.OK: {
const signals = response.value;
if (!isKeyValueObject(signals)) {
logWarning(basicErrorMessage, [
Expand All @@ -126,10 +178,10 @@ async function fetchAndValidateTrustedSignals(
}
return;
}
case FetchJsonStatus.NETWORK_ERROR:
case FetchStatus.NETWORK_ERROR:
// Browser will have logged the error; no need to log it again.
return;
case FetchJsonStatus.VALIDATION_ERROR:
case FetchStatus.VALIDATION_ERROR:
logWarning(basicErrorMessage, [
url.href,
": " + response.errorMessage,
Expand All @@ -138,3 +190,11 @@ async function fetchAndValidateTrustedSignals(
return;
}
}

function randomToken() {
return Array.prototype.map
.call(crypto.getRandomValues(new Uint8Array(16)), (byte: number) =>
byte.toString(/* radix= */ 16).padStart(2, "0")
)
.join("");
}
Loading

0 comments on commit 8358574

Please sign in to comment.