Repository navigation
Microsoft Advertising: reads, reports and campaign management - #130
Conversation
|
Thanks, Illia. Microsoft Advertising is the platform we had planned to build next, so we're glad to take this into the open-source package. We'd like to bring it to AdLoop Cloud later; at that point we'll take over maintenance of the module and may restructure it on our side, with credit to you. Please rebase this onto the shared-changes PR (see my comment on #127) instead of #127, so Microsoft can land independently of ChatGPT Ads. Must fix
Should fix
Before merge We ran the reads and a first write against our own account (78eeab6): A redacted sandbox (or live) transcript of the remaining write operations, please (ad groups, keywords and bids, RSAs, extensions, negatives and lists, targeting updates, pause/enable/remove). |
…ies, shared draft helpers - confirm_and_apply refuses a plan whose operation was blocked after the draft (BLOCKED, dry run and apply alike), logs the refusal, and keeps the plan pending. The confirmation dialog is not shown for such a plan. - safety/outcomes.py: PartialApplyError. An applier raises it when part of a plan reached the account or a create's outcome is unknown; confirm_and_apply retires the plan and answers PARTIAL_APPLY or UNKNOWN_OUTCOME with what was created. - safety/guards.py: blocked_operation_error, store_draft and blocked_plan_refusal, used by Reddit and Tag Manager instead of their own copies. - Approval card: Microsoft Advertising and ChatGPT Ads labels, as in the confirmation dialog.
Microsoft Advertising (Bing) through the REST API v13: Entra sign-in (authorization code + PKCE, device code as fallback), account, campaign, ad group, keyword, ad and extension reads, performance, search-term and location reports, the conversion setup audit, and campaign management behind the draft -> preview -> confirm_and_apply gate: budgets, status, campaigns and targeting, ad groups, keywords and bids, negatives and shared lists, RSAs, sitelinks, callouts, snippets, call and image extensions, and removal. Rebased onto the shared write-gate change instead of kLOsk#127.
- Broad match only with conversion-based bidding (MaxConversions, MaxConversionValue, TargetCpa, TargetRoas); Maximize Clicks no longer counts. Moving to any other strategy is refused while active broad keywords exist, at the draft and again at the dry run and apply; the keyword read's insight follows the same rule. - remove_microsoft_entity reads a campaign criterion at the draft, names its type and target, and refuses removing the last targeted location (the campaign would serve everywhere), again at the dry run. - Request bodies, snapshots and ids move from plan.changes to ChangePlan.apply_only_payload; preflight and apply read plan.apply_payload(). Drafts use the shared store_draft and blocked_operation_error. - MaxConversions without an active conversion goal is refused at the draft and the dry run (Microsoft refused such a create live with ConversionGoalCriteriaNotMetForBiddingScheme) and names the alternatives. - Throttling: codes 117 and 207 (and HTTP 429) raise at once with retry_after 60 instead of retrying after 5 s. - Plans store their campaign and ad group, so dry runs and applies read three lists instead of every ad group of the account; campaign reads ask once per campaign for its targeting (CriterionType Targets, with the per-type queries as fallback). - Reads: criterion type Radius, Audience Network inherited from the campaign when an ad group has no adjustment of its own; new campaigns warn about the Audience Network default. - Sign-in binds a free localhost port (Entra ignores the port), keeps listening until the state matches, and writes the token through an O_EXCL temp file. A bare 105 after a working sign-in is explained: the identity is not a user of the account, often because its users sign in with Google, which is not supported yet. - languages=['All'] is refused; target_cpa and max_cpc on a strategy that cannot use them are refused instead of dropped. - Dead code removed; the schedule limit is 7 windows a day throughout.
manage.py (2.4k lines, two parallel ~20-branch dispatchers) becomes the package microsoft_ads/manage: _common (constants, request and plan helpers, bidding, Step and Handler), campaigns, targeting, ad_groups, negatives, ads, extensions and removal. Each area drafts its plans and registers its operations in HANDLERS (operation -> dry-run check, apply); the package merges them into one table that preflight and apply_plan look up. The code of every check and apply moved unchanged; imports from adloop.microsoft_ads.manage keep working.
… one query
Found in the live run: CampaignCriterions/QueryByIds refuses the "Targets"
shorthand (4503 CampaignCriterionTypeInvalid, "Invalid API Campaign
Criterion Type : Targets on API tier"), which adds, updates and deletes
accept. The types joined by commas ("Location,LocationIntent,Radius") are
accepted, so get_microsoft_campaigns now makes one criterion call per
campaign; the per-type fallback stays for a refusal. The test fakes refuse
"Targets" in a query, as Microsoft does.
78eeab6 to
8fb66c1
Compare
|
Thanks, Daniel, and glad Microsoft fits. You taking it over for Cloud later is fine with me. All done; the branch is force-pushed. Landing
Must fix
Should fix
Before merge
Full suite: 2221 passed, 2 skipped. |
|
Merged, thanks Illia. Everything from the review is in, and the transcript made it easy to check. We re-ran the core of our live test on 8fb66c1 against our own account:
Two notes:
|
This adds Microsoft Advertising (Bing, Yahoo, AOL and DuckDuckGo search ads) as a new toolset,
microsoft_ads, through the REST API v13. It uses the same draft → preview →confirm_and_applygate as Google Ads and Reddit.Builds on #134 (the shared write-gate change), not on #127, so it can land on its own. The commits are the Microsoft module rebased onto upstream 0.40.1 and #134, then the review fixes (4651a2c), the split of
manage.pyinto per-area handlers (f2a4ca4), and one more fix from the live run (8fb66c1).Reads: accounts, campaigns (budget, bidding, languages, locations and location intent), ad groups, keywords, ads, performance with impression share, search terms, location performance, a conversion-setup audit (UET tags, goals, campaign goals), negatives and extensions. Rows carry the status of every parent level.
Writes:
Sign-in. You need a developer token and an Entra app registration (public client flows, delegated
msads.manage, redirecthttp://localhost/callback; Entra ignores the port for localhost, and the listener binds a free one). Thenadloop microsoft-loginruns the authorization code flow with PKCE in the browser. The device code flow stays as--device-code, but directories with security defaults block it (sign-in error 530035), so the browser is the default. The newoauth_redirect.pyis the one-shot localhost listener. It binds loopback on a free port, checks the path, keeps listening until a request carries the right state, and gives every connection an absolute deadline, so a client that trickles its request can't hold the sign-in open.CredentialsProvidergainsmicrosoft_ads_credentials(). A hosted provider without it doesn't get the tools, andhealth_checkreportsnot_configured.The gate. Microsoft has no validate-only mode, so the dry run re-reads the target and refuses any field the plan changes that was edited since the preview. That includes the target CPA and the ad schedule's time zone mode, which Microsoft returns only when asked through
ReturnAdditionalFields. The dry run also re-checks:max_daily_budget, explaining Microsoft's 30.4 × daily monthly spendmax_bid_increase_pcton ad group and keyword bids, including a keyword that falls back to the ad group bidThe real apply runs the same checks first.
Some applies take several requests, like a campaign and then its targeting, or a list and then its attachments. If a later step fails, or Microsoft accepts a batch only in part, the apply returns
PARTIAL_APPLYwith the IDs that exist. If a create's response never arrives, it returnsUNKNOWN_OUTCOME. Both retire the plan, because these creates have no idempotency and a replay would create them twice. This issafety/outcomes.PartialApplyErrorfrom #134.Schedules. Microsoft can't edit a DayTime window in place, windows may not overlap, and a day holds at most seven. So a replacement keeps unchanged windows, adds new ones first only where they overlap nothing and the day has room, deletes the old ones, then adds the rest. When that briefly leaves the campaign with no schedule, the preview says so. If the last step then fails, the error names the windows that stay, or says the campaign is unscheduled when none do. The preview also says whether the hours follow the account's time zone or each searcher's.
Smaller things worth knowing:
ConversionsQualifiedcolumns, since the oldConversionscolumns are deprecated and read 0 in keyword and search-query reports.draft_keywordsdoes for Google Ads. The preview showsstatus_on_createand the parent statuses, and warns when the keywords serve as soon as they're applied.Payloads: request bodies, snapshots and ids live in
ChangePlan.apply_only_payload.changesholds names and before → after, which the dialog and the card show as they are.Cloud contract:
TOOLSETSgainsmicrosoft_ads, soconfig/toolsets.phpand the runtime filter need the slug.Review fixes are listed in the review reply below. The live transcript of every write is linked there.
Image extensions.
draft_microsoft_image_extensionadds images to search ads on a campaign, an ad group or the account, one image extension per image. Images come from local files, public URLs or AdLoop uploads, through the same loading asdraft_image_assets, and each is pinned by checksum.POST Media, asGenericImage), creates the extensions, then links them. A failure after the upload returnsPARTIAL_APPLYwith the created media ids (and extension ids, if any) and retires the plan.get_microsoft_ad_extensionsnow lists image extensions and ad-group-level links, with alt text, display text, layouts, media ids and aremove_id. Each image row also hasimages: media type, size and aurlthat opens the picture, so a photo can be checked before its link is removed. The association query leavesImages,DisplayTextandLayoutsout, so the read asks for them throughAdExtensions/QueryByIdswithReturnAdditionalFieldsand then readsMediaMetaData/QueryByIds. If that second pass fails, the rows stay and an insight says the details are missing.remove_microsoft_entityaccepts ad-group remove ids, and unlinking keeps the extension and its images in the account library.LandscapeImageMediafollows Microsoft's .NET sample and bulk example. Image extensions can also carry search ads into native placements (Microsoft's audience ads guide), which the rules doc points out.What it does not do:
adloop initstep. It's configured by hand plusadloop microsoft-login, as the README says.Tested with the full suite plus
tests/test_microsoft_ads.py,tests/test_microsoft_ads_manage.pyandtests/test_oauth_redirect.py. The management tests drive the real gate against a stateful fake of the Campaign Management API. The fake also behaves like Microsoft in a few places: optional fields come back only when requested, overlapping DayTime windows are refused, and batches can partly fail or lose their response. The listener tests fail on the old listener. I've run the reads, report tools, drafts and dry runs against a live ad account. Real applies have since gone to that account too: new campaigns with their ad groups and keywords, RSAs, shared negative lists and their attachments, sitelinks, callouts and a structured snippet, a bid strategy change, ad group bids, and pause, enable and remove. That live use is where the in-store pilot error (28ed3d0) turned up. The image extension tool has since run live too: two campaigns got six images each, and the fixed read shows the picture, size, display text and layouts for every image extension on the account.