2019年12月2日 星期一

FB 廣告投放筆記

FB廣告投放

2018 年夏天跑去聽了一門泰國老闆的經商之術,2019年秋天尾則是看到FB廣告大神的臨時加開課程就衝了一發 XD 本身不算這個領域的,但每次衝去接觸就有新的感觸,真的不錯!

這算是我第二次廣告投放聽課,上一次是在新創內部課程,距今大概 5 年了吧。聽這課程還滿不錯的,花個短短的四小時複習一下有哪些新東西,跟哪些容易遺忘的東西。剛好近兩年在搞流量變現,現在把思維反過來用。

這門課採用實務教學方式,過程沒有投影片,而是一頁頁瀏覽器直接操作 FB 廣告投放的後台,有許多一言難以道盡的心得,條列式如下:
  • FB User 興趣、地理位置、社群互動資訊,依舊是強項,這是 Google Ads 難以對抗的項目。消費產品的電商就是靠 FB Ads 啦
  • Online-Merge-Offline (OMO) 廣告投放早已很成熟,店家透過 Point of sale (POS) 店頭機系統可以將線下活動資料匯入到 FB 廣告後台,即可做到上線線下整合,例如對到店用餐的客戶投放線上購物的資訊,這兩者是互補而非互斥,例如實體餐飲店的體驗轉換成線上購買新式甜點等。而線下為了收集到客戶的 Email 和手機幾乎無所不用,辦個優惠/贈品活動都是(想起來那些送化妝贈品要留姓名電話的過程嗎?)
  • 不要期待自流量帶來的交易,且在 3C 產業不要再對已購買的再行銷。對既有客戶再行銷屬於耗材類的商品才有意義。
  • 不需太強調 A/B Test 的策略,實際上運作就會不斷追求更佳文案,自然會演化到最佳的路線(就像寫程式不見得一定不用靠 framework ,久了為了好維護就會自成 framework 的)
  • FB 為了用戶體驗也會評分你的文宣,依照評分機制評估該讓你的文宣給多少受眾觀看。鼓勵多和用戶互動,別人分享或留言都可以去按讚,大概可以參考蝦皮/全聯/政治/眾籌等其他小編文章,一樣有滿滿的互動。
  • 粗略引流成交的成本約 300-800 台幣,商品利潤有這等水準就該試試 FB Ads 買流量導購電商
  • 電商領域上,不要過度陷入到曝光價格/點擊價格的競局,重點還是訂單成交跟獲利
其中,我覺得最重要習得的策略是評估廣告文案的狀態。評估一則廣告文案是靠受眾佔比數量,若在條件中受眾的數量是 10萬,那實務上大概也只能接觸到 50% 而已,在接觸到 50% 之前,大概 5-10% 時就可以評估文案跟受眾的互動成績,以此決定是否要換文案或受眾。

這些評估策略的經驗還滿寶貴的,著重的不在廣告投放時間,而是更精準的受眾互動數據。

講師也提到一些概念,也驗證過去自己猜測的方向:
不要以為廣告一直打,用戶可能哪天想起會買
也就是廣告不是一直擺在那邊花錢,接著催眠自己哪天用戶會想起的。

實務上,有效的廣告投放可能一天內(最遲七天內)就會看到成效。而重複對一個用戶打廣告的方式也是用不同文案,永遠要避免同一個文案被一個用戶重複觀看,重複觀看可是傷了自己的荷包。

跟這類似的本業領域大概就是數據收集,不認真思考數據收集的目的/情境,就全部說收,殊不知網路儲存要費用、數據運算的算力也是要費用,搞到最後所有的收集就跟沒收集一樣,隨著時間而逝。當然也有反過來的,就是拼命想規劃情境收集,沒有明確的評鑑用途就淪為八卦資訊,數據價值又隨著時間衰減而無意義。

其中 A/B Test 就有那麼一點點味道,因為所有文案/策略都有時空背景,不見得訓練出某個經驗就可以一直沿用,搞到最後就是不需 A/B Test,永遠真槍實彈上場,心中自成一把尺可以評估該文案到底能不能再進步,因為久了就該略知哪些 CTR / CPC 是合理的、是可以再進步的。

這行業也是令人小抖,例如好不容易找到了高價值受眾,結果這款商品銷售完後,就得再打起精神再找下個受眾,這種沒完沒了的生活... XD 其實跟新創差不多,在時間限制下看誰先搶下市場。搶下市場或得資源後,就開始進入資本主義的保衛戰,建立更高的市場跨入門檻,一日復一日。這時就覺得當個投資人是多麼幸福的事,靠分散風險錢砸下去就好,不用浪費自己的青春年華啊。

2019年11月27日 星期三

[Go] 透過 Golang 操作系統內建 WebView @ macOS, WKWebView

最近在幫同事想想,到底有沒有什麼更方便的環境去協助同事分析網頁的組成。再加上自己想順便了解一下 golang 是如何跨平台的,就隨意找了這套:github.com/zserge/webview ,看了看,學到不少東西。

心得一:所謂的跨平台,是別人幫你做完所有苦力

在 webview.h 裡頭就定義清楚在 Windows / Linux / macOS 上所使用的 WebView 元件為何,超佛心的幫你串好常見的用法

心得二:macOS 的 WKWebView 就跟 iOS 內的沒啥兩樣

簡稱想要做到更細的東西,都得靠 hack ,黑來黑去好累啊。像是想要追蹤一個網頁形成過程到底用了哪些 resource ,於似乎想要追蹤所有 request ,接著又要想想該如何向 UIWebView 那麼方便查看,接著又要想想是不是要用 WKURLSchemeHandler ,又該怎樣把系統內定的 http/https 黑回來 NSURLProtocol 處理等等,還是要搞個 proxy mode 追蹤?

想著想著...啊不就換套 CEF 就好 XD

於是乎,我就放棄 zserge/webview 在 macOS 上操弄 WKWebView !不過也趁這個機會我練了一下怎樣從 C call ObjectiveC 或者說從 Go 怎樣操作 ObjectiveC,也是總緣份吧

這次操弄 zserge/webview 大概玩了:

- 欣賞 zserge/webview 大大怎樣提供跨平台呼叫瀏覽網頁的元件

- 從 C 操作 WKWebView 物件,接著再呼叫它的 methods 換掉 User-Agent 資訊

WEBVIEW_API void webview_set_user_agent(struct webview *w, const char *user_agent) {
  // https://developer.apple.com/documentation/webkit/wkwebview/1414950-customuseragent?language=objc
  objc_msgSend(w->priv.webview, sel_registerName("setCustomUserAgent:"), get_nsstring(user_agent));
}


- 從 C 操作 WKWebView 物件,接著再呼叫它的 methods 添加監聽 didStartProvisionalNavigation 和 didFinishNavigation

static void wk_webview_didStartProvisionalNavigation(id self, SEL cmd, id webView, id navigation) {
  // https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455621-webview?language=objc
  webview_print_log("at wk_webview_didStartProvisionalNavigation");
  //id absoluteString = objc_msgSend(objc_msgSend(webView, sel_registerName("URL")), sel_registerName("absoluteString"));
  webview_print_log( get_webview_current_url(webView) );
}

static void wk_webview_didFinishNavigation(id self, SEL cmd, id webView, id navigation) {
  // https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455629-webview?language=objc
  webview_print_log("at wk_webview_didFinishNavigation");
}

// ...

  Class __WKNavigationDelegate = objc_allocateClassPair(
      objc_getClass("NSObject"), "__WKNavigationDelegate", 0);
  class_addProtocol(__WKNavigationDelegate,
                    objc_getProtocol("WKNavigationDelegate"));

// ...

  class_addMethod(
      __WKNavigationDelegate,
      sel_registerName(
          "webView:didStartProvisionalNavigation:"),
      (IMP)wk_webview_didStartProvisionalNavigation, "[email protected]:@@");
  class_addMethod(
      __WKNavigationDelegate,
      sel_registerName(
          "webView:didFinishNavigation:"),
      (IMP)wk_webview_didFinishNavigation, "[email protected]:@@");


收工!

2019年11月25日 星期一

jq 指令筆記 - 使用 to_entries 保留 key/value 資料,再用 select / index / match 過濾資料

人生就是有那種怪怪的堅持,明明寫個 php 或 python 就立刻可以解掉的需求,偏偏愛用 jq 來處理 XD

故事是來自於有個 key-value pair 的 json 資料:

% echo '{"A":{"field":"v1"},"B":{"field":"v2"}}' | jq ''
{
  "A": {
    "field": "v1"
  },
  "B": {
    "field": "v2"
  }
}


想要透過 jq 過濾時,也能保留 key 資料,這時就用 to_entries 來達成:

% echo '{"A":{"field":"v1"},"B":{"field":"v2"}}' | jq 'to_entries[]'
[
  {
    "key": "A",
    "value": {
      "field": "v1"
    }
  },
  {
    "key": "B",
    "value": {
      "field": "v2"
    }
  }
]

% echo '{"A":{"field":"v1"},"B":{"field":"v2"}}' | jq 'to_entries[]'
{
  "key": "A",
  "value": {
    "field": "v1"
  }
}
{
  "key": "B",
  "value": {
    "field": "v2"
  }
}


接著要再過濾指定欄位帶有 關鍵字 時,就靠 select 跟 index 來達成:

% echo '{"A":{"field":"v1"},"B":{"field":"v2"}}' | jq 'to_entries[] | select( .value.field | index("v2") >= 0 )'
{
  "key": "B",
  "value": {
    "field": "v2"
  }
}


此例是輸出 value.field 數值帶有 v2 關鍵字。而 index 之外的還有 match 等支援 regular expression 的用法,只是要用 !match 時有點卡卡串不太起來,就乾脆用 index 來處理。

2019年11月24日 星期日

[JS] Google Adsense 自動廣告 - 關閉 錨定廣告/重疊廣告 方式

前陣子跟 Google Adsense 的新加坡客戶經理聊過幾句,被她引導開啟了一些自動化廣告最佳化的項目。最近發現公司的網站出現 錨釘 廣告,想說是不是不小心誤上程式碼,最後想起應當是自動化廣告的機制。

對於一些頁面不想開啟置頂或置底的錨定廣告,可用以下方式關閉:

<script>
(adsbygoogle = window.adsbygoogle || []).push({
enable_page_level_ads: false
});
</script>


剛好去年是要手動靠 enable_page_level_ads: true 啟用的,一時之間還以為實驗的程式碼不小心發布了 :P 因為去年研究時感受到他很容易覆蓋掉一些網路應用的版面,如導覽功能,因此決議不讓他上線。

2019年11月20日 星期三

[PHP] OAuth / Sign in with Apple JS - 使用 Apple JS SDK 讓網站支援 Apple ID 登入

SignInWithApple00

最近幫看同事串 Sign In with Apple 好像有很多不順的地方,拿自己的 Apple developer 帳號試試 :P 相關文件:
首先就來闖關吧,先進行 Sign In with Apple 設定,需要指定某個 email domain 給 Apple 跟用戶溝通,此例用 appid.changyy.org 網域為例:

Certificates, Identifiers & Profiles -> More -> Sign In with Apple -> Configure

SignInWithApple03

在添加網域進去之前,請記得先設定 SPF DNS Record,不然 Apple 一驗證不合 SPF 時,又得等 DNS Cache 更新等到天荒地老 Orz

在此先添加 Type=TXT 的 DNS Record 吧!

v=spf1 include:amazonses.com -all

SignInWithApple02

接著用 dig 驗一下:

% dig -t txt appid.changyy.org

; <<>> DiG 9.10.6 <<>> -t txt appid.changyy.org
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 47410
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;appid.changyy.org. IN TXT

;; ANSWER SECTION:
appid.changyy.org. 119 IN TXT "v=spf1 include:amazonses.com -all"

;; Query time: 56 msec
;; SERVER: 8.8.8.8#53(8.8.8.8)
;; WHEN: Wed Nov 20 00:48:09 CST 2019
;; MSG SIZE  rcvd: 92


添加 appid.changyy.org 網域,按下 Register 時,接著努力驗證通過即可。如果沒有先添加 SPF 就會出現類似訊息:The domain 'appid.changyy.org' is not SPF compliant.

接著再回到 Apple Developer 網站繼續按 Register (不是立刻通過,要等 DNS Cache 過期),就可以進行後續的認證了,如 https://appid.changyy.org/.well-known/apple-developer-domain-association.txt 配置等。接著又得搞 https 連線,又進入了 免費SSL/TLS憑證 - Let's Encrypt 與 NGINX 的設定 XD 在此不贅述。

終於可以進入 Apple Developer Account 其他設定了,首先要建立 Services IDs 時,會要求有一個 App ID 為 primary App ID ,這件事也代表 Sign In with Apple 的核心還是 App ,此例新建一個 App ID = org.changyy.apple.app-id 為例,並且在下方勾選 Sign In with Apple 且 Enable as a primary App ID。

接著,建立 Services IDs :

Certificates, Identifiers & Profiles -> Identifiers -> Add -> Services IDs -> 建立一個 org.changyy.sign-in-with-apple 並啟用 Sign In with Apple -> Configure -> Web Domain = appid.changyy.org 而 Return URLs = https://appid.changyy.org/callback.php 並按 Add 和 Save -> 再按 Continue 完成

SignInWithApple08

最後,再來建立一組 Key 用來溝通:

Certificates, Identifiers & Profiles ->  Keys -> 添加一組 Key Name = SignInWithAppleKey,記得勾選 Sign In With Apple -> 點擊 Configure 挑選完 App ID 按 Save -> 最後會下載一個 AuthKey_KeyID.p8 檔案,就是後續溝通的項目。

SignInWithApple10

SignInWithApple11

如此一來,在上述的過程中可以得到以下關鍵物:

  • Service ID Identifier = org.changyy.sign-in-with-apple (後續是 OAuth 的 Client_ID)
  • Key ID = 在 *.p8 的檔名上,或是在 Certificates, Identifiers & Profiles -> Keys -> SignInWithAppleKey 瀏覽可看到
  • Team ID = 在 Apple Developer 登入後右上角可看見,或是在 Certificates, Identifiers & Profiles -> Identifiers -> 隨意一組 App ID -> App ID Prefix 就有標記 (Team ID) 資訊
  • Return URLs = 在 Service ID 內編輯的,如 https://appid.changyy.org/callback.php

把這些資訊弄個 JSON 紀錄:

$ cat settings.json
{
        "CLIENT_ID": "org.changyy.sign-in-with-apple",
        "SCOPES": "name email",
        "REDIRECT_URI": "https://appid.changyy.org/callback.php",
        "STATE": "",
        "TEAM_ID": "YourTeamID",
        "KID" : "YourKeyID",
        "Key_P8_PATH" :"../keystore/AuthKey_YourKeyID.p8",
        "" : ""
}


最後把 https://github.com/changyy/sign-in-with-apple-js 拿來用,在根目錄建立 keystore 並擺放 AuthKey_YourKeyID.p8 以及上述 settings.json 擺在 /path/sign-in-with-apple-js/php/settings.json ,在把 https://appid.changyy.org/ Document_Root 設定在此專案的 /path/sign-in-with-apple-js/php 目錄上,如此用 https://appid.changyy.org/ 時,就會有以下畫面:

SignInWithApple12

點擊後就會被引導完成 Apple 登入,登入後會導向到 https://appid.changyy.org/callback.php 並且看到簡單的 OAUTH code 的使用:
oepnssl = OpenSSL 1.1.1  11 Sep 2018
jwt_header = Array
(
    [typ] => JWT
    [alg] => ES256
    [kid] => YourKeyID
)

jwt_payload = Array
(
    [iss] => YourTeamID
    [iat] => 1574184061
    [exp] => 1574187661
    [aud] => https://appleid.apple.com
    [sub] => YourServiceID
)

apple_public_keys = Array
(
    [keys] => Array
        (
            [0] => Array
                (
                    [kty] => RSA
                    [kid] => AIDOPK1
                    [use] => sig
                    [alg] => RS256
                    [n] => lxrwmuYSAsTfn-lUu4goZSXBD9ackM9OJuwUVQHmbZo6GW4Fu_auUdN5zI7Y1dEDfgt7m7QXWbHuMD01HLnD4eRtY-RNwCWdjNfEaY_esUPY3OVMrNDI15Ns13xspWS3q-13kdGv9jHI28P87RvMpjz_JCpQ5IM44oSyRnYtVJO-320SB8E2Bw92pmrenbp67KRUzTEVfGU4-obP5RZ09OxvCr1io4KJvEOjDJuuoClF66AT72WymtoMdwzUmhINjR0XSqK6H0MdWsjw7ysyd_JhmqX5CAaT9Pgi0J8lU_pcl215oANqjy7Ob-VMhug9eGyxAWVfu_1u6QJKePlE-w
                    [e] => AQAB
                )

        )

)

apple_public_key_pem = 

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlxrwmuYSAsTfn+lUu4go
ZSXBD9ackM9OJuwUVQHmbZo6GW4Fu/auUdN5zI7Y1dEDfgt7m7QXWbHuMD01HLnD
4eRtY+RNwCWdjNfEaY/esUPY3OVMrNDI15Ns13xspWS3q+13kdGv9jHI28P87RvM
pjz/JCpQ5IM44oSyRnYtVJO+320SB8E2Bw92pmrenbp67KRUzTEVfGU4+obP5RZ0
9OxvCr1io4KJvEOjDJuuoClF66AT72WymtoMdwzUmhINjR0XSqK6H0MdWsjw7ysy
d/JhmqX5CAaT9Pgi0J8lU/pcl215oANqjy7Ob+VMhug9eGyxAWVfu/1u6QJKePlE
+wIDAQAB
-----END PUBLIC KEY-----


apple_public_key_alg = RS256


[Lcobucci\JWT]
request data = Array
(
    [client_id] => YourServiceID
    [client_secret] => A.B.C1
    [code] => c
    [grant_type] => authorization_code
    [redirect_uri] => https://YourServiceIDReturnURL
)


response = Array
(
    [access_token] => a
    [token_type] => Bearer
    [expires_in] => 3600
    [refresh_token] => r
    [id_token] => id_token
)



[Firebase\JWT]
request data = Array
(
    [client_id] => YourServiceID
    [client_secret] => A.B.C2
    [code] => c
    [grant_type] => authorization_code
    [redirect_uri] => https://YourServiceIDReturnURL
)


response = Array
(
    [error] => invalid_client
)



[Firebase\JWT and \Lcobucci\JWT\Signer\Ecdsa\MultibyteStringConverter]
request data = Array
(
    [client_id] => YourServiceID
    [client_secret] => A.B.C3
    [code] => c
    [grant_type] => authorization_code
    [redirect_uri] => https://YourServiceIDReturnURL
)


response = Array
(
    [error] => invalid_grant
)



[Firebase\JWT without openssl_pkey_get_private and \Lcobucci\JWT\Signer\Ecdsa\MultibyteStringConverter]
request data = Array
(
    [client_id] => YourServiceID
    [client_secret] => A.B.C4
    [code] => c
    [grant_type] => authorization_code
    [redirect_uri] => https://YourServiceIDReturnURL
)


response = Array
(
    [error] => invalid_grant
)
網路上滿多人在討論 firebase/jwt 的用法,但其實在 2019/11/20 來看,firebase/jwt 仍就沒有完整 Sign In with Apple 所需的 JWT 編碼格式,我則是靠 Lcobucci/JWT 套件完成打通  Sign In with Apple 的,並且看懂為何 firebase/jwt 還不能打通 :P 拿著 Lcobucci/JWT 內的 MultibyteStringConverter 來小試身手果真就通了。再找個時間貢獻 firebase/jwt 來修正好了,這次為了檢驗 firebase/jwt 失敗問題,大概至少看了 5套 php-jwt 的寫法,結果大多都是從 luciferous/jwt fork 擴充的,這可是 2011 的程式呢。

而跟 Apple Auth API 溝通的結果,若本身 JWT 製作踩到演算法等問題只會收到 {"error":"invalid_client"},但這也包含 OAUTH REDIRECT_URI 不合法等等,如果演算法打通了,而 Code 過期或是被重複使用時,會收到 {"error":"invalid_grant"} 資訊。