Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/fopost/fopost-cli
go 1.22

require (
github.com/fopost/fopost-go v0.2.1-0.20261003222702-faf657bdaf90
github.com/fopost/fopost-go v0.2.1-0.20261003224903-429911602cac
github.com/spf13/cobra v1.10.2
golang.org/x/term v0.27.0
)
Expand Down
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ github.com/fopost/fopost-go v0.2.1-0.20260920195809-0f2a3f293638 h1:HyHOHnuVqcv8
github.com/fopost/fopost-go v0.2.1-0.20260920195809-0f2a3f293638/go.mod h1:Qz+7UCYTBnDEeZqB3VMrePxmYLVR9rFvx3+QWcKND8Q=
github.com/fopost/fopost-go v0.2.1-0.20261003215548-65ec0bbbe207 h1:mHYksMJZWjyhMiJPmJOxa4fxvjUyaBkYjog9GWM+MnU=
github.com/fopost/fopost-go v0.2.1-0.20261003215548-65ec0bbbe207/go.mod h1:Qz+7UCYTBnDEeZqB3VMrePxmYLVR9rFvx3+QWcKND8Q=
github.com/fopost/fopost-go v0.2.1-0.20261003222702-faf657bdaf90 h1:vzeOG95v+uy3sTuLYiv0PdDAIGMBGNofSD/NTB0hQ1o=
github.com/fopost/fopost-go v0.2.1-0.20261003222702-faf657bdaf90/go.mod h1:Qz+7UCYTBnDEeZqB3VMrePxmYLVR9rFvx3+QWcKND8Q=
github.com/fopost/fopost-go v0.2.1-0.20261003221824-a57f7c3c6032 h1:83hQv33R0XK8D2Zgn2B/EdcBJRJQ0at+3T0Tp+EJS7c=
github.com/fopost/fopost-go v0.2.1-0.20261003221824-a57f7c3c6032/go.mod h1:Qz+7UCYTBnDEeZqB3VMrePxmYLVR9rFvx3+QWcKND8Q=
github.com/fopost/fopost-go v0.2.1-0.20261003224903-429911602cac h1:icF8vsLmR1YuWbs+e+NYFkApcxq3x1IN3565jdCBEVo=
github.com/fopost/fopost-go v0.2.1-0.20261003224903-429911602cac/go.mod h1:Qz+7UCYTBnDEeZqB3VMrePxmYLVR9rFvx3+QWcKND8Q=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
Expand Down
148 changes: 148 additions & 0 deletions internal/cmd/ads_google.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ func newAdsGoogleCmd(state *State) *cobra.Command {
"insights are on the other ads commands and work across networks.",
}
cmd.AddCommand(
newGoogleRecommendationsCmd(state),
newGoogleOptimizationScoreCmd(state),
newGoogleKeywordsCmd(state),
newGoogleKeywordIdeasCmd(state),
newGoogleSearchTermsCmd(state),
Expand Down Expand Up @@ -353,3 +355,149 @@ func newGoogleQueryCmd(state *State) *cobra.Command {
flags.bind(cmd)
return cmd
}

func newGoogleRecommendationsCmd(state *State) *cobra.Command {
flags := &googleFlags{}
var types []string
var apply, dismiss []string
var yes bool
cmd := &cobra.Command{
Use: "recommendations",
Short: "Read, apply or dismiss what Google suggests for the account",
Long: "Without --apply or --dismiss this lists what Google suggests, with the\n" +
"impact it projects. Applying one changes what the live account serves or bids.",
RunE: func(cmd *cobra.Command, _ []string) error {
client, err := state.Client()
if err != nil {
return err
}
scope, err := flags.scope(state)
if err != nil {
return err
}
printer := state.Printer()

if len(apply) > 0 && len(dismiss) > 0 {
return usageErrorf("give --apply or --dismiss, not both")
}

if len(apply) > 0 || len(dismiss) > 0 {
ids := apply
verb := "Applied"
run := client.GoogleAds.ApplyRecommendations
if len(dismiss) > 0 {
ids, verb, run = dismiss, "Dismissed", client.GoogleAds.DismissRecommendations
}
if !yes {
if err := confirm(state, fmt.Sprintf(
"%s %d recommendation(s) on %s?", verb, len(ids), flags.customer)); err != nil {
return err
}
}
count, err := run(cmd.Context(), &fopost.GoogleRecommendationsRequest{
GoogleScope: scope,
IDs: ids,
})
if err != nil {
return err
}
return printer.Value(map[string]int{strings.ToLower(verb): count}, func() {
printer.Success("%s %d recommendation(s)", verb, count)
})
}

rows, err := client.GoogleAds.Recommendations(cmd.Context(), scope, types)
if err != nil {
return err
}
return printer.Value(rows, func() {
table := [][]string{}
for _, row := range rows {
table = append(table, []string{
row.ID,
row.Type,
deltaOf(impactClicks(row)),
deltaOf(impactConversions(row)),
})
}
printer.Table([]string{"id", "type", "clicks", "conversions"}, table)
})
},
}
flags.bind(cmd)
cmd.Flags().StringSliceVar(&types, "type", nil, "Limit to a recommendation type; repeatable")
cmd.Flags().StringSliceVar(&apply, "apply", nil, "Apply this recommendation id; repeatable")
cmd.Flags().StringSliceVar(&dismiss, "dismiss", nil, "Dismiss this recommendation id; repeatable")
cmd.Flags().BoolVarP(&yes, "yes", "y", false, "skip the confirmation prompt")
return cmd
}

func impactClicks(row fopost.GoogleRecommendation) (*float64, *float64) {
if row.Impact == nil {
return nil, nil
}
return row.Impact.BaseClicks, row.Impact.PotentialClicks
}

func impactConversions(row fopost.GoogleRecommendation) (*float64, *float64) {
if row.Impact == nil {
return nil, nil
}
return row.Impact.BaseConversions, row.Impact.PotentialConversions
}

// deltaOf renders what applying a recommendation would move the metric by.
func deltaOf(base, potential *float64) string {
if base == nil || potential == nil {
return "-"
}
change := *potential - *base
if change >= 0 {
return fmt.Sprintf("+%.1f", change)
}
return fmt.Sprintf("%.1f", change)
}

func newGoogleOptimizationScoreCmd(state *State) *cobra.Command {
flags := &googleFlags{}
cmd := &cobra.Command{
Use: "optimization-score",
Short: "Read the account's optimization score and each campaign's",
RunE: func(cmd *cobra.Command, _ []string) error {
client, err := state.Client()
if err != nil {
return err
}
scope, err := flags.scope(state)
if err != nil {
return err
}
score, err := client.GoogleAds.OptimizationScore(cmd.Context(), scope)
if err != nil {
return err
}
printer := state.Printer()
return printer.Value(score, func() {
printer.Line("Account: %s", percentOf(score.Score))
rows := [][]string{}
for _, campaign := range score.Campaigns {
rows = append(rows, []string{
campaign.ID,
output.Truncate(campaign.Name, 40),
percentOf(campaign.Score),
})
}
printer.Table([]string{"id", "campaign", "score"}, rows)
})
},
}
flags.bind(cmd)
return cmd
}

func percentOf(score *float64) string {
if score == nil {
return "-"
}
return fmt.Sprintf("%.0f%%", *score*100)
}
Loading