@koda-sl/baker-cli 0.194.0-dev.5dba041c4 → 0.194.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/{chunk-5YX3QOT3.js → chunk-O5GXFPWC.js} +4 -4
- package/dist/chunk-O5GXFPWC.js.map +1 -0
- package/dist/cli.js +44 -27
- package/dist/cli.js.map +1 -1
- package/dist/engine/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-5YX3QOT3.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -47,7 +47,7 @@ import {
|
|
|
47
47
|
toModelSafeImage,
|
|
48
48
|
ulid,
|
|
49
49
|
validateCanvasDeep
|
|
50
|
-
} from "./chunk-
|
|
50
|
+
} from "./chunk-O5GXFPWC.js";
|
|
51
51
|
import {
|
|
52
52
|
csvOrJson,
|
|
53
53
|
daysAgoIso,
|
|
@@ -15740,6 +15740,7 @@ var PLACEMENT_TARGETING_DOC = "https://developers.facebook.com/docs/marketing-ap
|
|
|
15740
15740
|
var CONSTRAINED_PLACEMENTS = [
|
|
15741
15741
|
{
|
|
15742
15742
|
position: "facebook_reels_overlay",
|
|
15743
|
+
reportedAs: "facebook_reels_overlay",
|
|
15743
15744
|
platform: "facebook",
|
|
15744
15745
|
kind: "constraint",
|
|
15745
15746
|
note: "Ads on Facebook Reels take two shapes and a stock 9:16 video fits neither: overlay ads are 'a single, static image ad' (JPG/PNG, 1:1, 0% ratio tolerance, headline capped at 10 characters), and post-loop ads are 1:1 video of 4-15 seconds. Expect a long-headline 9:16 creative to look wrong here.",
|
|
@@ -15748,6 +15749,7 @@ var CONSTRAINED_PLACEMENTS = [
|
|
|
15748
15749
|
},
|
|
15749
15750
|
{
|
|
15750
15751
|
position: "explore_home",
|
|
15752
|
+
reportedAs: "instagram_explore_grid_home",
|
|
15751
15753
|
platform: "instagram",
|
|
15752
15754
|
kind: "constraint",
|
|
15753
15755
|
note: "Marketing API placement asset customization states 'The explore_home placement only supports the SINGLE_IMAGE format', but Meta's own Ads Guide lists Instagram Explore home under video specs. Check the account's play rate for this position before treating it as image-only.",
|
|
@@ -15756,6 +15758,7 @@ var CONSTRAINED_PLACEMENTS = [
|
|
|
15756
15758
|
},
|
|
15757
15759
|
{
|
|
15758
15760
|
position: "right_hand_column",
|
|
15761
|
+
reportedAs: "right_hand_column",
|
|
15759
15762
|
platform: "facebook",
|
|
15760
15763
|
kind: "correction",
|
|
15761
15764
|
note: "Facebook right column publishes full video specs (MP4/MOV/GIF, 1:1 at 1440x1440 or mobile-only 4:5 at 1440x1800, 1 second to 241 minutes) \u2014 it is NOT image-only. Its real constraint is that it cannot be the only placement for a video, collection, or canvas ad.",
|
|
@@ -15767,30 +15770,30 @@ var FACEBOOK_PREVIEW_FORMATS = {
|
|
|
15767
15770
|
feed: "MOBILE_FEED_STANDARD",
|
|
15768
15771
|
right_hand_column: "RIGHT_COLUMN_STANDARD",
|
|
15769
15772
|
marketplace: "MARKETPLACE_MOBILE",
|
|
15770
|
-
|
|
15773
|
+
facebook_stories: "FACEBOOK_STORY_MOBILE",
|
|
15771
15774
|
facebook_reels: "FACEBOOK_REELS_MOBILE",
|
|
15772
15775
|
facebook_reels_overlay: "FACEBOOK_REELS_BANNER",
|
|
15773
15776
|
instream_video: "INSTREAM_VIDEO_MOBILE",
|
|
15774
15777
|
video_feeds: "INSTREAM_VIDEO_MOBILE"
|
|
15775
15778
|
};
|
|
15776
15779
|
var INSTAGRAM_PREVIEW_FORMATS = {
|
|
15777
|
-
|
|
15778
|
-
|
|
15779
|
-
|
|
15780
|
-
|
|
15781
|
-
|
|
15782
|
-
|
|
15783
|
-
|
|
15780
|
+
feed: "INSTAGRAM_STANDARD",
|
|
15781
|
+
instagram_stories: "INSTAGRAM_STORY",
|
|
15782
|
+
instagram_reels: "INSTAGRAM_REELS",
|
|
15783
|
+
instagram_explore: "INSTAGRAM_EXPLORE_CONTEXTUAL",
|
|
15784
|
+
instagram_explore_grid_home: "INSTAGRAM_EXPLORE_GRID_HOME",
|
|
15785
|
+
instagram_search: "INSTAGRAM_SEARCH_CHAIN",
|
|
15786
|
+
instagram_profile_feed: "INSTAGRAM_PROFILE_FEED"
|
|
15784
15787
|
};
|
|
15785
|
-
var PLATFORM_SCOPED_POSITIONS = /* @__PURE__ */ new Set(["
|
|
15788
|
+
var PLATFORM_SCOPED_POSITIONS = /* @__PURE__ */ new Set(["feed"]);
|
|
15786
15789
|
function previewFormatForPosition(position, platform) {
|
|
15787
15790
|
if (platform === "facebook") return FACEBOOK_PREVIEW_FORMATS[position];
|
|
15788
15791
|
if (platform === "instagram") return INSTAGRAM_PREVIEW_FORMATS[position];
|
|
15789
15792
|
if (PLATFORM_SCOPED_POSITIONS.has(position)) return void 0;
|
|
15790
15793
|
return FACEBOOK_PREVIEW_FORMATS[position] ?? INSTAGRAM_PREVIEW_FORMATS[position];
|
|
15791
15794
|
}
|
|
15792
|
-
function placementFormatNote(
|
|
15793
|
-
return CONSTRAINED_PLACEMENTS.find((p) => p.
|
|
15795
|
+
function placementFormatNote(reportedPosition) {
|
|
15796
|
+
return CONSTRAINED_PLACEMENTS.find((p) => p.reportedAs === reportedPosition);
|
|
15794
15797
|
}
|
|
15795
15798
|
function positionsMissingRequiredFeed(targeting) {
|
|
15796
15799
|
const positions = targeting?.facebook_positions;
|
|
@@ -17525,8 +17528,8 @@ var metaDraftCommand = defineCommand62({
|
|
|
17525
17528
|
import { defineCommand as defineCommand63 } from "citty";
|
|
17526
17529
|
|
|
17527
17530
|
// src/commands/ads/meta/placement-delivery.ts
|
|
17528
|
-
var MIN_IMPRESSIONS =
|
|
17529
|
-
var
|
|
17531
|
+
var MIN_IMPRESSIONS = 20;
|
|
17532
|
+
var SILENT_FRACTION_OF_REFERENCE = 0.1;
|
|
17530
17533
|
var PLAYING_REFERENCE_RATE = 0.5;
|
|
17531
17534
|
function toNumber(value) {
|
|
17532
17535
|
const n = Number(value);
|
|
@@ -17576,10 +17579,17 @@ function placementDeliveryHints(rows) {
|
|
|
17576
17579
|
const carriesVideoMetrics = rows.some((row) => Array.isArray(row.video_play_actions));
|
|
17577
17580
|
if (!carriesVideoMetrics || !isSingleAdQuery(rows)) return [];
|
|
17578
17581
|
const placements = aggregate(rows);
|
|
17579
|
-
const
|
|
17580
|
-
|
|
17581
|
-
const
|
|
17582
|
-
if (
|
|
17582
|
+
const judgeable = placements.filter((p) => p.impressions >= MIN_IMPRESSIONS);
|
|
17583
|
+
const impressions = judgeable.reduce((total, p) => total + p.impressions, 0);
|
|
17584
|
+
const reference = impressions > 0 ? judgeable.reduce((total, p) => total + p.plays, 0) / impressions : 0;
|
|
17585
|
+
if (reference < PLAYING_REFERENCE_RATE) return [];
|
|
17586
|
+
const silent = judgeable.filter((p) => p.playRate < reference * SILENT_FRACTION_OF_REFERENCE).sort((a, b) => b.impressions - a.impressions);
|
|
17587
|
+
if (silent.length === 0) {
|
|
17588
|
+
const worst = [...judgeable].sort((a, b) => a.playRate - b.playRate)[0];
|
|
17589
|
+
return [
|
|
17590
|
+
`Checked every placement above ${MIN_IMPRESSIONS} impressions against this ad's own play rate (${(reference * 100).toFixed(0)}%): no placement is failing to play the video${worst ? `, the lowest being ${worst.platform}/${worst.position} at ${(worst.playRate * 100).toFixed(0)}%` : ""}. So placements do not explain a "it shows as a still image" report \u2014 rule out autoplay off or data saver in the viewer's own app, which renders every video as its cover frame account-wide.`
|
|
17591
|
+
];
|
|
17592
|
+
}
|
|
17583
17593
|
const hints = silent.map((placement) => {
|
|
17584
17594
|
const note = placementFormatNote(placement.position);
|
|
17585
17595
|
const documented = note ? ` Meta documents this placement: ${note.note}` : "";
|
|
@@ -17712,6 +17722,11 @@ function shouldAutoAsync(body) {
|
|
|
17712
17722
|
}
|
|
17713
17723
|
return false;
|
|
17714
17724
|
}
|
|
17725
|
+
function wantsAsync(asyncFlag, body) {
|
|
17726
|
+
if (asyncFlag === true) return true;
|
|
17727
|
+
if (asyncFlag === false) return false;
|
|
17728
|
+
return shouldAutoAsync(body);
|
|
17729
|
+
}
|
|
17715
17730
|
function applyDateRange(body, args) {
|
|
17716
17731
|
const range = getDateRange(args);
|
|
17717
17732
|
if (range.datePreset) body.datePreset = range.datePreset;
|
|
@@ -17760,9 +17775,9 @@ function buildBody(args) {
|
|
|
17760
17775
|
async function pollAsync(reportRunId) {
|
|
17761
17776
|
const start = Date.now();
|
|
17762
17777
|
while (Date.now() - start < POLL_MAX_MS) {
|
|
17763
|
-
const status = await
|
|
17764
|
-
|
|
17765
|
-
{}
|
|
17778
|
+
const status = await apiGet(
|
|
17779
|
+
"/api/ads/meta/insights/async/status",
|
|
17780
|
+
{ "report-run-id": reportRunId }
|
|
17766
17781
|
);
|
|
17767
17782
|
if (status.async_status === "Job Completed") return;
|
|
17768
17783
|
if (status.async_status === "Job Failed" || status.async_status === "Job Skipped") {
|
|
@@ -17860,8 +17875,10 @@ Async is automatic for heavy queries; pass --async to force it, or --no-async to
|
|
|
17860
17875
|
"use-account-attribution": { type: "boolean", description: "Use account-saved attribution setting" },
|
|
17861
17876
|
"use-unified-attribution": { type: "boolean", description: "Use ad-set unified attribution setting" },
|
|
17862
17877
|
limit: { type: "string", description: "Max rows (default 1000)" },
|
|
17863
|
-
async
|
|
17864
|
-
|
|
17878
|
+
// Declaring `no-async` too would be a trap: citty resolves it to `async: false`
|
|
17879
|
+
// and never fills an arg of that name, so the declaration reads as supported
|
|
17880
|
+
// while nothing consumes it.
|
|
17881
|
+
async: { type: "boolean", description: "Force the async submit-and-poll path (--no-async refuses it)" },
|
|
17865
17882
|
"no-sort": { type: "boolean", description: "Skip default spend-desc sort" },
|
|
17866
17883
|
"skip-cache": { type: "boolean", description: "Bypass server-side cache" },
|
|
17867
17884
|
output: { type: "string", description: "Output format", default: "json" }
|
|
@@ -17880,7 +17897,7 @@ Async is automatic for heavy queries; pass --async to force it, or --no-async to
|
|
|
17880
17897
|
}
|
|
17881
17898
|
const body = buildBody(args);
|
|
17882
17899
|
const fmt = csvOrJson(args);
|
|
17883
|
-
const wantAsync =
|
|
17900
|
+
const wantAsync = wantsAsync(args.async, body);
|
|
17884
17901
|
try {
|
|
17885
17902
|
const rows = wantAsync ? await runAsync(body) : await apiPost("/api/ads/meta/insights", body);
|
|
17886
17903
|
const sorted = args["no-sort"] ? rows : sortRowsBySpendDesc(rows);
|
|
@@ -18093,11 +18110,11 @@ function resolvePlacement(position, platform) {
|
|
|
18093
18110
|
}
|
|
18094
18111
|
if (PLATFORM_SCOPED_POSITIONS.has(position) && !platformArg) {
|
|
18095
18112
|
failPreviewValidation(
|
|
18096
|
-
`--placement ${position} names a different surface on Facebook and on Instagram \u2014 add --platform facebook or --platform instagram`
|
|
18113
|
+
`--placement ${position} names a different surface on Facebook and on Instagram \u2014 add --platform facebook or --platform instagram (the breakdown row's publisher_platform)`
|
|
18097
18114
|
);
|
|
18098
18115
|
}
|
|
18099
18116
|
failPreviewValidation(
|
|
18100
|
-
`No Meta preview format is published for placement "${position}". Meta does not document one for every placement (
|
|
18117
|
+
`No Meta preview format is published for placement "${position}". Meta does not document one for every placement (facebook_notification and facebook_profile_feed among them), so this placement cannot be previewed \u2014 report it as unverified rather than inferring what it renders. Pass the platform_position exactly as the insights breakdown spelled it: targeting names a few surfaces differently (story, reels, explore_home) and those names never appear in a breakdown. Previewable placements: ${listPlacements()}`
|
|
18101
18118
|
);
|
|
18102
18119
|
}
|
|
18103
18120
|
function listPlacements() {
|
|
@@ -18132,7 +18149,7 @@ Examples:
|
|
|
18132
18149
|
type: "string",
|
|
18133
18150
|
description: "platform_position from an insights breakdown (e.g. facebook_reels_overlay) \u2014 resolved to an ad_format"
|
|
18134
18151
|
},
|
|
18135
|
-
platform: { type: "string", description: "facebook|instagram \u2014
|
|
18152
|
+
platform: { type: "string", description: "facebook|instagram \u2014 required for --placement feed, which both report" },
|
|
18136
18153
|
"list-formats": { type: "boolean", description: "Print the ad_format values and placement mappings, then exit" },
|
|
18137
18154
|
"out-file": { type: "string", description: "Save iframe HTML to this path" },
|
|
18138
18155
|
"skip-cache": { type: "boolean", description: "Bypass server-side cache" }
|