2020年6月20日 星期六

[Javascript] Shopify App - 修正 location.replace 與 GA 追蹤碼 utm_* 的應用

認真使用 Shopify 大概也有一個多月了,使用了一些付費 Shopify App 在做本地化加強,發現這些第三方 app 還滿善用 variant 機制,原先是拿來設計一個商品有多種顏色等用途,現在則是應用在多儲存一些在地化的資訊。

因此,當發現需要轉化成在地化資訊時,會透過接近導網址架構,多添加 variant 資訊:

var new_url = window.location.protocol + '//' + window.location.host + window.location.pathname + '?variant=' + variant_id;

然而這樣的小機制,卻把 utm_* 的追蹤碼都給去掉了 XD 所以在幫他補強一下:

var new_url = window.location.protocol + '//' + window.location.host + window.location.pathname + '?variant=' + variant_id;

if (window.location && window.location.href) {
var m = window.location.href.match(/[\?&]([^=]+=[^&]+)/g);
if (m) {
var params = null;
for (var i=0; i<m.length ; ++i) {
if (m[i].indexOf('utm_') > 0) {
if (params == null) {
params = m[i].substr(1);
} else {
params += '&' + m[i].substr(1);
}
}
}
if (params != null) {
new_url += '&' + params;
}
}
}

收工!

2020年6月7日 星期日

[PHP] 解決 CodeIgniter 4 Command line 輸出緩衝/不即時的問題 (ob_start / ob_end_flush) @ macOS 10.15

有點忘記了,好像在 CodeIgntier 3 (CI3) 建置 Command line 運行的任務時,可以很即時看到運算的輸出結果(預設是 testing mdoe?),但在 CI 4 時,卻必須等到所有任務都做完了,才會一次輸出結果。這對於在 Jenkins 系統運行時,無法時看到運行過程是很不方便的。

追蹤 CI 4 的架構,起因是 system/CodeIgniter.php:run 中,運行時呼叫了 Events::trigger('pre_system'); ,在 app/Config/Events.php 可以看到定義了 pre_system 的工作內容,大意就是當 ENVIRONMENT 不是 testing 時,就會開啟 ob_start 使用機制。

在 CI 架構上,使用 ob_start 也有個好處,那就是寫一些網頁服務時,有時是內容產出跟 header 送出夾雜在一起。當在 PHP 使用 header 送出資料時,是不允許前面已經輸出過資料的。這時靠 ob_start / ob_end_flush 應用,就可以完美排除這種問題。

回過頭來,面對 command line 的任務,若想要排除 ob_start 的影響,可以試著包裝一個輸出訊息的函式,並在裡頭輸出完資料後,再靠這兩段補強輸出資料,如:

function debug($message) {
echo "[DEBUG] $message\n";
while (\ob_get_level() > 0)
\ob_end_flush();
}

或是乾脆在負責 Command line 任務的建構子中,直接靠 ob_get_level 和 ob_end_flush 清掉輸出緩衝的設計:

public function __construct() {
while (\ob_get_level() > 0)
\ob_end_flush();
}

如此就可以得到即時的輸出資料了

2020年6月2日 星期二

[Linux] AddTrust External CA Root expired 處理方式 @ Ubuntu

週日也不幸踩到這個雷了,當時只解了幾個,沒想到這個範圍很大很大。
故事是我們採用 Namecheap 服務來簽署 Wildcard SSL 憑證,而使用到 AddTrust External CA Root 簽署。

當 AddTrust External CA Root 有效期間只到 2020/05/30 10:48:38 時,時間一到後,導致用戶連到我們的服務產生失敗,以 curl 來說,他預設依賴 OS 提供的系統憑證資訊:

$ curl https://ourservice.exmaple.com
curl: (60) server certificate verification failed. CAfile: /etc/ssl/certs/ca-certificates.crt CRLfile: none
More details here: http://curl.haxx.se/docs/sslcerts.html

curl performs SSL certificate verification by default, using a "bundle"
 of Certificate Authority (CA) public keys (CA certs). If the default
 bundle file isn't adequate, you can specify an alternate file
 using the --cacert option.
If this HTTPS server uses a certificate signed by a CA represented in
 the bundle, the certificate verification probably failed due to a
 problem with the certificate (it might be expired, or the name might
 not match the domain name in the URL).
If you'd like to turn off curl's verification of the certificate, use
 the -k (or --insecure) option.

而 Chrome Browser 很佛心,從軟體層自動幫轉到 USERTrust RSA Certification Authority。

問題的解法:

正確解法:直接再重新簽核新的憑證,新的憑證簽署時會略過已過期的項目。
臨時解法:將 https client side 的 AddTrust External CA Root 註解起來 

臨時解法,以 Ubuntu 內的 https client 為例,將 mozilla/AddTrust_External_Root.crt 那行註解起來

$ sudo vim /etc/ca-certificates.conf && sudo sh -c 'apt update && apt install ca-certificates && update-ca-certificates -f -v'

臨時解法只能解決 https client 是已知的範圍,像是服務的 client 都是自己控制的,這時就可以先解決那些 client 憑證的檢驗機制,但追到更細時,大部分的 https client 靠 OS 提供的憑證資訊就只要改一次,如果 https client 上跑 node.js 等應用,他們可能會依賴其他相關套件來維護憑證資訊,不一定會用 OS 內紀錄的憑證資訊,這時要排除會非常痛苦。因此,正確解法是重新簽發憑證

2020年5月19日 星期二

Shopify 開店筆記 - 跨境電商、依重量計算運費、依地區限制購物

google SHOP stock 2020/05/16

約莫協助公司維運自家電商已經有五年多。

通常電商的成績不佳,大部分會被認為是電商平台的設計問題,像是購物流程是否可以改善、網站配色是否不夠精美等等。因此,我們也開始研究是否朝向知名的網購平台 Shopify 來維運。Shopify 是一個體質非常好的電商平台,非常適合小試身手的平台,而這波疫情也讓 Shopify 股價扶搖直上。

其實,公司維運的電商平台老早就有用 Shopify 幾年了,如幫其他客戶經營副牌、白牌等。對我而言是一個熟悉又陌生的平台,因為整體上的維運都已經不需要透過工程師。趁這次把品牌電商遷移到 Shopify 上,順手記錄碰到的課題,主要分成收錢(金流)、寄貨(物流)、買家交易過程和其他需求等筆記,而 Shopify 其實有不錯的 Shopify App Store,可以自行添購(通常是訂閱式,每月支付),有些真的滿夠用,十分方便。

收錢/金流:

提供常見的 Paypal 支付,以及 Shopify Payment 機制,而這兩點都恰好因為已有 Paypal 帳號跟有香港辦公室的關係無痛打通。這兩個金流的使用都有地緣限制,像 Shopify Payment 還可以有 Google pay / Apple Pay ,只是不見得買家的地方可以使用。

金流上當客戶在網站上完成支付後,Shopify 便會立即發信給 商店聯絡電子郵件,就是在 "後台 -> 設定 -> 一般 -> 商店聯絡電子郵件" 設定。

此外 Paypal 帳號也是只限制 PayPal 商業帳戶,不能用個人的。

寄貨/物流:

Shopify 的物流可以簡單到自己寄送即可,單純把訂單標記成已出貨,並且可以填寫 tracking code 及查詢的方式(使用的物流),就完成了!

而物流的複雜度其實可大可小,我認為比較重要的是運送費率的設計,Shopify 支援設定多個倉儲位置,假設設定了兩個處(香港和台灣),那必須替這兩個倉儲分別設定運送策略。假設只對香港設定寄送到全世界而台灣倉庫沒有任何運送設定,這時有一位台灣用戶下單時,他會被默認靠近台灣而使用台灣倉庫的寄送策略,但台灣倉庫沒設定運送到台灣的運費規則,而導致使用者無法完成下單(會顯示類似無法寄送的錯誤訊息)。

另外,若設計以重量方式計算運費時,假設以 0.5kg 為間隔單位,分別設定了 "小於0.5kg"、"0.5kg到1.0kg"、"1.0kg到1.5kg",1.5kg到2.0kg",這時若使用者買的東西總重落在 2.0kg 以下是可以的,且有運費合併計算的美意,但如果使用者買的總重量超過 2.0kg 時,一樣會落入無法寄送的錯誤訊息,因為沒有大於 2.0kg 的運費規則。

另外,也要留意裝箱的箱子重量,定義了依重量計費時,結果不如預期時,可能是箱子重量影響。

買家交易過程:

如果沒有調整過預設設定時,客戶在下單過程時,可以選用手機門號簡訊完成認證,快速驗身完就可以下單了,非常貼心。而訂單狀態會用簡訊通知對方。然而,若是在做跨境電商時,碰到訂單問題時,就得走電話聯繫,推論會十分痛苦。另一方面,快遞都會要求填寫聯絡電話,若客戶訂購時沒留下電話資訊也會很苦的。

因此,將會到 "後台 -> 設定 -> 結帳",在 "客戶聯絡" 方面,改成限定用 email 結帳。表單資訊,會要求填寫 "運送地址電話號碼"。作為跨境電商的訂單管理機制。

其他項目 - 賣樣品給客人:

Shopify 建立訂單草稿 是非常好用的項目!若業務想要銷售樣品給客人時,透過訂單草稿,可以自行打包幾個商品、給予折扣,再透過寄送電子發票,便直接通報客戶直接到 Shopify 付款,整個過程非常方便。讓業務很快地辦完事,負責金流物流的同事也只是例行處理自己的職責。十分完美。

可以多多參考 Shopify 的說明:訂單草稿

其他項目 - 提醒未完成結帳項目:

當用戶在 Shopify 平台下單時,未完成結帳的訂單,一樣在後台可以看到。Shopify 可以在 "後台 -> 設定 -> 結帳 -> 未完成結帳作業" ,設定幾小時後傳送提醒信件。如此可以設法拉回一些潛在用戶。

其他項目 - 跨境電商 - 依照地區給予不同價格:

這個應數主要是總代理跟代理之間的關係,如果某個地區有代理商了,總代理賣的價格通常不能低於代理商,不然代理商都賣不出貨了。面對這個需求有兩種解法,第一種就是故意在電商上不銷售到有代理商的區域,可以靠 運費費率 的設定,讓有代理商地區沒有運費規則,這樣用戶就不能下單。另一種要花點錢的機制,使用 Multi-Country-Pricing 等類似服務,依照地區給予不同價格,但 Shopify 的核心只有收一個幣別,就算透過 Multi-Country-Pricing 的機制能夠給予當地使用者觀看當地的幣值,最終在收費時,還是會轉回原先 Shopify 商店所規範的價格(如美金為單位)


其他項目 - 信件通知:

Shopify 會用 "後台 -> 設定 -> 一般 -> 商店詳細資訊 -> 客戶電子郵件" 當作 mail sender 發送。通常 Shopify 發送到自己管理的 mail server 時,可以透過 SPF record 來允許,增加信件不被規範到垃圾信中。只有時很慘的,自家 mail server 阻擋了 Shopify 寄來的信時,只能設法靠不同的 mail domain 來避開。


在不同 mail domain 避開的機制下,例如現在用 [email protected] 作為 商店聯絡電子郵件 ,只好再用 [email protected] 當作 客戶電子郵件 。此時必須記得去修改信件通知的資訊,避免信件通知信一直請客人寄信給 [email protected] 尋求客服。而 [email protected] 最好也在設定個自動轉寄到 [email protected] ,避免漏掉客戶的重要信件。願意花錢可以靠 AWS WorkMail 單一帳號一個月四美金頂著用,不願意花錢可以試試免錢的 Yandex.Mail 。

2020年5月8日 星期五

[Python] 使用 http.server.BaseHTTPRequestHandler 製作簡易 Proxy 機制 @ macOS, python 3

學會用 curl / wget 模仿一些 request 後,在一些特殊情境上,還是得弄個 proxy 出來,雖然有 man-in-the-middle Proxy: mitmproxy 可以使用,但有時就是想要單純一點,寫點小程式自娛一下 XD

故事的情境:

- 想用 VLC 播放一些 Streaming 來源,但該 streaming 在存取時要求多塞一些request header,直接播會收到 40X 回應
- VLC 預設只吃 OS Level 的 proxy 設定

基於上述情境,雖然靠 curl/wget 添加 request header 可以搞定,但 VLC 這類就無法達成播放指定來源時多添加 request header。

因此,除了靠 OS Level 的 Proxy server 添加 reader header,就剩寫一隻代抓資料,邊抓邊輸出 stream 的小小程式,程式碼:

import http.server
import urllib.parse
import requests
import time

class ProxyHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
	protocol_version = 'HTTP/1.0'

	def do_GET(self, body=True):
		try:
			target_url = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query).get('url', None)
			print("[INFO] target_url: "+str(target_url))
			if target_url != None:
				target_url = target_url[0]
				req_header = self.parse_headers()
				#resp = requests.get(target_url, headers=req_header, verify=False, stream=True)
				resp = requests.get(target_url, headers=req_header, verify="certs.pem", stream=True)
				print("[INFO] resp.status_code: %d " % resp.status_code)
				if resp.status_code == 404:
					self.send_response(404)
					self.send_header('Content-Type','text/html')
					self.end_headers()
					self.wfile.write("NOT FOUND".encode())
				else:
					self.send_response(resp.status_code)
					for k in resp.headers.keys():
						print("[INFO] resp.headers: [%s][%s]" % (k, resp.headers[k]) )
						self.send_header(k, resp.headers[k])
					self.end_headers()
					for chunk in resp.iter_content(chunk_size=1024):
						if chunk:
							self.wfile.write(chunk)
							self.wfile.flush()
						else:
							time.sleep(0.05)
				
			else:
				self.send_response(404)
				self.send_header('Content-Type','text/html')
				self.end_headers()
				self.wfile.write("NOT FOUND".encode())
		finally:
			pass

	def parse_headers(self):
		req_header = {}
		for line in self.headers:
			line_parts = [o.strip() for o in line.split(':', 1)]
			if len(line_parts) == 2:
				req_header[line_parts[0]] = line_parts[1]
		return self.inject_header(req_header)
    
	def inject_header(self, headers):
		headers['Referer'] = 'https://example.com/'
       
		return headers
	

if __name__ == '__main__':
	server_address = ('0.0.0.0', 8081)
	httpd = http.server.HTTPServer(server_address, ProxyHTTPRequestHandler)
	print('http server is running')
	httpd.serve_forever()

如此跑起來後,就可以用 http://localhost:8081/?url=https://exmaple.com/streaming 機制,例如 VLC 軟體直接輸入 http://localhost:8081/?url=https://exmaple.com/streaming 位置來嘗試播放。