見出し画像

Twitter広告のコンバージョンAPIをサーバーサイドGTMで実装する方法を解説 - 前編

いよいよ、Twitter広告でもコンバージョンAPIのアナウンスがありましたね!

ウェブサイトのコンバージョントラッキング https://business.twitter.com/ja/help/campaign-measurement-and-analytics/conversion-tracking-for-websites.html

Twitter広告ヘルプセンター

今回はこれをサーバーサイドGTM(ssGTM)を使って実装できないか?
という記事になります。

結論を最初に述べてしまうと
ssGTMだけで実装するのは難しそうでした。

GTMのカスタムテンプレートで使用できる関数は
カスタムテンプレート独自のJS関数(API)を使う必要がありますが、
OAuth認証の部分を実装できそうなAPIがないんですよね。

サーバーサイド タグ設定 API  |  Google Tag Manager - Server-side  |  Google Developers https://developers.google.com/tag-platform/tag-manager/server-side/api?hl=ja

Google Developers

※ 例えばCryptoJSのライブラリをサイトにロードする方法を使えば、GTMだけで完結も可能です。今回はサイトにJavaScriptのライブラリなどはロードしない方法で記載していきます。

そのため色々組み合わせて実装していこうと思います。
今回の記事と次の記事の最終ゴールは以下です。

ssGTM 85%、Python(+FastAPI)15%で実装
ssGTM 40%、Google Apps Script(GAS) 60%で 実装

認証の部分も頑張ればできる可能性はあるのですが
他に委ねてしまった方が早いと結論しました。

正直、『①はPythonじゃないといけない』とか
『②はGASじゃないといけない』とかは全くありません。

ただ、WEBマーケティングや広告をやっている人の中では
GASがとっつきやすいのではないかと思い、
②では比較的汎用的なGASをチョイスしました。

①についてはほぼssGTMだけで頑張って、
『”ここだけは”という一部』を他でやることを考えた場合、
その一部についてはPythonで実装するのが簡単そうだったので選びました。
(※筆者の扱える言語の中では)


Ⅰ.この記事の流れ

というわけで今回のテーマの最終目的は
ssGTM 85%、Python(+FastAPI)15%で実装
ssGTM 40%、Google Apps Script(GAS) 60%で 実装
になるわけです。。

が、最初から完成形を載せるだけではつまらないですし、
個人的なメモも兼ねて試行錯誤の流れをありのまま記載していきます。

となると、膨大なボリュームになりますので
今回のテーマは2部構成にさせてください。

第一部の「本note記事」の流れは
TwitterでCAPIを実装する概要 →
  Pythonで実装 → nodejsで実装 → GASで実装

という流れになります。

ここを抑えた上で、「次のnote記事」で実際にssGTMを活用して
ssGTM 85%、Python(+FastAPI)15%で実装
ssGTM 40%、Google Apps Script(GAS) 60%で 実装
のところをやっていきたいと思います!!

(※PythonにはTwitterのOAuth 1認証の記述をサックリかける便利なライブラリがあり、深いこと考えずにパパッと書けそうだったのでまずはPythonから試してみました。)

ちなみに、本記事(前編・後編ともに)では、Pythonやnodejsの実行環境の構築までは触れていません。ローカルに環境構築する方法は検索すればすぐ出てくると思います。

個人的にはローカルで環境構築するよりdockerでコンテナを立てて、その上で環境構築することをおすすめしますが、dockerの話を書き出すとそれだけで1記事できそうなので、割愛させてください。

さて、では早速いきましょう!!

最初は概要ということで、TwitterのコンバージョンAPIを実装するにあたり
必要な情報などを整理しましょう。

Ⅱ.Twitter コンバージョンAPIの 概要

まず、大枠の流れです。
※まだ概要だけなので、何言ってるかよく分からなくても大丈夫です。

  1. Ads APIにアクセスするのに必要な、API_KEYやACCESS_TOKENをゲットする

  2. Conversion APIのイベントを作成する

  3. Conversion イベントのリクエスト(headerとbody)を作成する

  4. 「1-3」を使ってエンドポイントにリクエスト送る

と、まぁこんな感じになります。

これらを順番に見ていきましょう。

1. Tokenまわりを整える

まずは必要なTokenをゲットしてきましょう。

以下の手順に従えば問題なく取得できるはずです。
(こちらのページは最近日本語verも用意されました)

まずTwitterにログインします。
(会社で使用する場合は会社のアカウントにログインしましょう)

そしてdeveloperのダッシュボードにアクセスします。https://developer.twitter.com/en/portal/dashboard

開発者用のアカウントがなければ、以下のページにリダイレクトするはずです。

国とか、どんなケースで使うの?とか聞かれるので、任意で入力して「Let's do this」

途中の「Will you make Twitter content or derived information available to a government entity or a government affiliated entity?」は「No」にしておいた方が無難です。
(政府や関連機関の利用がある場合は「Yes」にした方がいいかもしれません)

するとポリシー同意画面になるので、チェックして「Submit」を押します。

なお、プロフィールで電話番号を登録していないと、Submitできずにエラーが出ます。

これで申請が通れば、開発者ポータルのダッシュボードから「APP ID」を入手可能です。

以下2023年8月更新

以下の方法は古い方法のため「取り消し線」で消しております。
最新の方法は下の方に記載しております。

次に、Twitter Ads APIを利用するための申請をします。

色々記載すべきところがあるのですが、かいつまんで簡単に説明します。

App ID⇒上記で入手した「APP ID」

「How will you use the Ads API? *」は「Conversion API」を選択

あとは文章で説明しなくてはならないところがあります。
ちょっと面倒かもしれませんが、いい感じに280文字以内で(英語で)記載していただければOKです。

例えば、以下のようなものがあります。
※この3つ以外にも文章を記載しなくてはいけないところは複数あります

...tell us about your business * ⇒ ビジネスについて
...what do you plan to build on the Ads API? * ⇒ Ads APIでなにするの?
...describe your unique differentiators in the Ad Tech ecosystem * ⇒ 独自の強みは?

↑↑は古い方法で、現在はGoogleフォームから申請する形になっております。
以下のフォームから必要事項を記載して申請してください。

この申請が通ると、広告APIアプリケーションを所有することになり、
開発者ポータルからアクセストークンなどを生成できるようになります。

開発者ポータルのプロジェクト > アプリで、タブを「Keys and tokens」にすると、「API Key and Secret」と「Access Token and Secret」が生成できるようになるので、generateしましょう。
(そしてこれは一度閉じると、2度は見られないため、もしメモを取り忘れたら、Regenerateする必要があります)

なお、今回は「Bearer Token」は使いませんので、スルーでOKです。

これで、
API_KEY , API_KEY_SECRET, ACCESS_TOKEN , ACCESS_TOKEN_SECRET
を入手できることになります。

2. Conversion APIのイベント作成

Twitter広告の管理画面での作業になります。
こちらはもう慣れている人も多いかもしれませんね。

管理画面に入り「ツール>イベントマネージャー」へ進みます。

なお、2022年9月現在、イベントマネージャーの管理画面の変遷期のようで、読み込みするたびに新しいverになったり古いverになったりして、やや不安定です。。。ここでは新しいverで進めます。

すでに、WEBでTwitter広告を実施したことがある場合は下記画面のように
Twitterピクセル→ウェブID:hogehoge とあると思います。
ない場合は「イベントソースを追加」をしてください。

Twitterのリファレンスなどを読んでいて出てくる「pixel_id」はこの「ウェブID」のことになります。

また、CAPIでリクエスト送信する際のエンドポイントURLは

https://ads-api.twitter.com/11/measurement/conversions/pixel_id

になります。

これは大事なので頭に入れておいてください。

後のコード例では

https://ads-api.twitter.com/11/measurement/conversions/hogehoge

のように記載しておりますが、
自身のpixel_id(ウェブID)に置き換えてください。

さて、コンバージョンAPIを実装するにはこのpixel_idだけでは足らず、
イベントを追加する必要があります。

イベントマネージャーの管理画面で、「イベントを追加」をクリック
(今度は「イベントソースの追加」ではありません)

そうすると以下のようになるので、名前やイベントの種類を決めましょう。
名前はなんでもいいです。

記載したら、「次」へ。

設定方法を選びます。
コンバージョンAPIを利用するなら、「コードを使ってイベントを定義」を選ぶ必要がります。

以下のページになるので、インストール方法で「コンバージョンAPI」のタブを選択。

ここに、ピクセルID(pixel_idやウェブIDと同義)と、
イベントIDを得られます。

イベントIDは、「tw-pixel_id-ランダムな文字列」なっています。
本記事のコード例では「tw-hogehoge-fugafuga」と書いていますので、
ここも自身のアカウントに合わせて置き換えてください。

ピクセルIDとイベントIDをメモしたら、「保存」して終了。
(メモしなくても後からでも確認はできます)

そうすると、イベントが作成されています。

ここに載っている「ID:~」の部分には、「tw-hogehoge-fugafuga」でいうところの「fugafuga」の部分しか記載していません。

コンバージョンAPIで必要なイベントIDは「tw-hogehoge-fugafuga」になりますので、間違わないようにしましょう。

※現時点では、「fugafuga」のみでもエラーにならず計測出来ていますが、
ここ数ヶ月(2022年8月-10月くらい)にかけて、Twitterの公式リファレンスも、イベントIDの項目が「fugafuga」→「tw-hogehoge-fugafuga」に書き換えられているため、今後は後者が本流となりそうです。

3. Conversion イベントのリクエスト作成

3-1. headerの作成

この記事の中で、ここが最も厄介です(笑)

ここでごちゃごちゃ説明するよりも、
正直以下の公式ページを見たほうが早いと思います。
※決して説明を投げ出しているわけではありません!!笑

・・・とはいえ、リンク貼るだけでは流石に芸がないので、
少し説明していきましょう。

まず、ヘッダーを作るのに『7つの項目(キーと値のペア)』が必要です。
キーを列挙すると以下のようになります。

  1. oauth_consumer_key

  2. oauth_nonce

  3. oauth_signature

  4. oauth_signature_method

  5. oauth_timestamp

  6. oauth_token

  7. oauth_version

1のoauth_consumer_key は 「1. Tokenまわりを整える」でゲットした「API_KEY」のことになります。

2のoauth_nonce は、いわゆるナンスですね。
認証などで用いられる、「一度限り有効なランダムなデータ」になります。

仮想通貨やブロックチェーンに明るい方なら聞いたことあると思うので、イメージしやすいですかね。

マイナーがマイニングして見つけてるものがナンスと呼ばれていますが
このナンスも「一度限り有効な数値列」ですよね。

よく分からん方は「認証で使うもの!」くらいに思っていてください。

この文字列は「ランダムな英数字文字列を生成する方法」であれば、特に決まりはありませんので、タイムスタンプとかでもOKです。

※一応、Twitterのおすすめは、32バイトのランダムな文字列をbase64でエンコードして、記号類を除く方法のようです。

3のoauth_signature。。。
本来ならこの「oauth_signature」の生成についてだけで
1つの記事ができそうなくらい、面倒です。

難しいことはしていないので、順を追って把握していけば理解できるかと思うのですが、いかんせん手順が多いのと若干ややこしい。。

以下に公式ページのリンクを記載します。
oauth_signatureについては以上とさせてください。(実際にコードを書く際にもう少し説明いたします。)

もし、詳しく説明がほしい、ということでしたら、別途記事にすることも考えますので、コメントなどでお知らせください。

また、本記事の後半では実際にコード書いているので、
コード見たほうがここは理解しやすいかもしれません。

4のoauth_signature_method。これは署名方式ですね。
そもそも、「署名」ってなんぞや?ってところですが、
ざっくり「リクエスト途中で改竄が行われていないか?」や「リクエストは間違いなく本人が送っているのか?」などを確認するためのものです。

Twitter APIではこの署名方式に「HMAC(Hash-based Message Authentication Code)」を利用しています。
さらに、HMACの中で使われるハッシュ関数は「SHA1」になります。

難しい話はおいておいて(興味ある人は調べてみてください)、
とりあえず「oauth_signature_method」に指定するのは「HMAC-SHA1」になります。これは変数とかではなく、この文字列("HMAC-SHA1")です。

5のoauth_timestampはシンプルです。
コンバージョンした時間のタイムスタンプですね。

6のoauth_tokenは特に難しいことはなく、
「1. Tokenまわりを整える」でゲットした「ACCESS_TOKEN」になります。

7のoauth_version はOAuth認証のバージョンですね。
今回は「"1.0"」を指定します。

これでヘッダーを作るのに必要な要素が出揃いました。
これで「めでたしめでたし」と言いたいところですが、
ここから実際にヘッダーを作るのがまた厄介です(笑)

  1. 最初に、文字列「OAuth 」を書きます。(最後の半角スペースも込み)

  2. キーをパーセントエンコードして、「1」の後に記載

  3. 文字列「=」を「2」の後に記載

  4. 値をパーセントエンコードして、「""(ダブルクォーテーション)」で囲み、「3」の後に記載

  5. キーと値のペアが残っている場合は、コンマ「,」とスペース「 」を「4」のあとに記載して、「2」に戻る

という感じですが、言葉で書いても意味不明すぎますね。

例をTwitterの公式ページから拝借すると、以下のような形になります。

OAuth oauth_consumer_key="xvz1evFS4wEEPTGEFPHBog", oauth_nonce="kYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg", oauth_signature="tnnArxj06cWHq44gCs1OSKk%2FjLY%3D", oauth_signature_method="HMAC-SHA1", oauth_timestamp="1318622958", oauth_token="370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9we​​JAEb", oauth_version="1.0"

Authorizing a request
by developer.twitter.com

パーセントエンコードについては下記を参照ください。

ここまで来たらあと少し!!
Authorizationの値は上記で作成したものになります。

あとは言語や使用するメソッドによっては、明示的にリクエストbodyの種類を伝える「Content-Type」を指定する必要があります。

言語にもよりますが、だいたい以下のような形でしょう。

headers = {
"Authorization": 「上で頑張って作った文字列(OAuth から始まるやつ)」,
"Content-Type": "application/json"
}

これでようやくヘッダーの完成です!

3-2. リクエストボディの作成

ボディ(payload)にも当然ルールがありますが、言葉でごちゃごちゃ書くより
実際のコードを見たほうがわかりやすいですかね。

言語による差異はありますが概ね以下のようになります。

※2022年11月追記
identifiersとして「hashed_phone_number」も使えるようになっていますが、今回の記事では考えていません。

少し説明しますと、単純に電話番号(例えば09012345678)をハッシュ化すれば良いわけではなく、国際電話の仕様に則ります。

つまり、「+9012345678」をハッシュ化したものを送ります。
(先頭の「+」記号も含んだ文字列をハッシュ化します)※

payload = {
    conversions:[
        {
            conversion_time: "2022-02-18T01:14:00.603Z",
            event_id: "tw-hogehoge-fugafuga",
            identifiers: [
                {
                    hashed_email: "63dc92389e3326e3ee3d7e6e715fda270977b9d293d97760f89105c86b3e2f11"
                },
                {
                     twclid: "23opevjt88psuo13lu8d020qkn"
                },
            ],
            value: '2000',
            price_currency: 'JPY',
            number_item: 2,
            transaction_id: 'abc123',
            contents: [
              {
                content_id: '12345',
                content_name: 'shoes',
                content_type: 'Apparel',
                content_price: '1000',
                num_items: 2
              }
            ]
        }
    ]
};

細かいことはAPIのリファレンスを見ましょう。

少しかいつまんで説明していきます。

基本的には「conversions」の配列の中に情報を詰め込んでいきます。

conversion_time
ISO 8601の形式です。現時点(2022年8月)ではUTCにしか対応していないっぽいので、わざわざ日本時間に直したりする必要はないです。

event_id
「2. Conversion APIのイベント作成」で手に入れた「イベントID」になります。前述したように、「tw-hogehoge-fugafuga」という形になります。
(hogehogeにはpixel_idが入る)

identifiers
twclid もしくは hashed_email 、少なくとも片方は必須です。
hashed_emaiはsha256でハッシュ化している必要があります。

twclidはTwitter クリックIDですね。
Twitterで広告をクリックした際のURLを見ると
twclid = hogehoge というパラメータがついているかと思いますが、
この「hogehoge」がそれにあたります。

また、Cookieの「_twclid」の中にもtwclidの情報はあります。
※Cookieの「_twclid自体」はPixelのバージョンなども含んだJSON文字列になっているため、そのままでは使えませんが、twclidの情報を抜き出すことは可能です。

その他のパラメータは特別複雑なことはないかなぁとは思います。

1点注意すべきは「value」と「content_price」ですね。
APIのリファレンスを見ると、『Type: double』となっています。
つまりダブル型ですね。

出典:API-Reference
by developer.twitter.com

ここ、「整数」でリクエスト送信しようとするとエラーが出ます。
なんとも面倒なことです。

例えば、「1000」はダメ。
なるほどなるほど。

ならば「1000.00」はどうか?・・・これもダメ。(えー)
「1000.0001」はOK。みたいな感じ。。

こーれは扱いずらいぞ、、と、
思っていたのですが、なぜか文字列にしたらエラー出ませんでした。。

なので、上の例でもそうですが、「'1000'」みたいな感じで文字列にしています。

というわけで文字列にすれば解決するのですが、
もう少し深堀りしたいと思います。
(深堀りという名のただの余談なので、飛ばしていい)

今回、ssGTMやGASを使っていくわけですが、
そのベースはJavaScriptです。

JavaScriptの数値型って細かく別れてないんですよね。

他の言語であれば、「1」はInteger(整数)、「1.0」はFloat(浮動小数点)みたいに役割が明確です。

JavaScriptでは、typeofで見てもらえば分かりますが、全て「number」として返ってきます。

typeof(135); // "number"
typeof(135.79); // "number"

ECMAScriptの仕様によると、JavaScriptのnumberは
倍精度浮動小数点(double型)で表現されています。

「じゃあデフォルトでdouble型じゃん!問題ないね!」
となりそうなのですが、なぜかそうじゃないみたい。。

ここは私もちゃんと解明できていないですが、
GTMやGASからは「1000」という整数や、「1000.00」のように小数点以下が0のものは、整数として送信するみたい。

「1000.0000001」とかならうまくいきます(笑)

ちなみに、Pythonでは「1000」では同様にエラーでるけど、
「1000.0」ではエラーでずにリクエスト送信できます

なので、Pythonは「1000.00」のように小数点以下が0の数値列も、double(float)としてTwitterに送信しているようですね。

はい、余談でした。
以上。

ようやく「3. Conversion イベントのリクエスト作成」が終わりました。
そしてここまでが、Twitter コンバージョンAPIの概要です。

おさらいですが、この記事の流れはこうです。

TwitterでCAPIを実装する概要 →
  Pythonで実装 → nodejsで実装 → GASで実装

まだ「最初」しか終わってない(笑)
さて、ここから概要の情報を元に実際にコード書いていきましょう。

Ⅲ. Pythonでリクエスト送信

Pythonには、request_oauthlibという便利ライブラリがあります。
これを使えば、上記でめちゃくちゃ面倒だったヘッダー構築の部分をすべて
Python内部でやってくれます。

下記をご覧ください。

from requests_oauthlib import OAuth1Session
import json


with open('tw_secret.json') as f:
    info = json.load(f)
    
CONSUMER_KEY = info['API_KEY']
CONSUMER_SECRET = info['API_KEY_SECRET']
ACCESS_TOKEN = info['ACCESS_TOKEN']
ACCESS_TOKEN_SECRET = info['ACCESS_TOKEN_SECRET']


oauth = OAuth1Session(
    CONSUMER_KEY,
    client_secret=CONSUMER_SECRET,
    resource_owner_key=ACCESS_TOKEN,
    resource_owner_secret=ACCESS_TOKEN_SECRET
)

conversion_time = '2022-09-19T05:10:46.750Z'


CAPI_URL = "https://ads-api.twitter.com/11/measurement/conversions/hogehoge"

headers = {
    "Content-Type": "application/json"
}

payload = {
    "conversions":[
        {
            "conversion_time": conversion_time,
            "event_id": "tw-hogehoge-fugafuga",
            "identifiers": [
                {
                    "hashed_email": "48ddb93f0b30c475423fe177832912c5bcdce3cc72872f8051627967ef278e08"
                }
            ],
            "value": 1000.0,
            "price_currency": "JPY",
            "number_item": 2,
            "conversion_id": "abc1111",
            "contents": [
                {
                    "content_id": "ab12",
                    "content_name": "shoes",
                    "content_type": "Apparel",
                    "content_price": float(500),
                    "num_items": 2
                }
            ]
        }
    ]
}

r = oauth.post(CAPI_URL, headers=headers, data=json.dumps(payload))

はい。。

これでいいんですよね!
めっちゃ簡単!!

コードでは「CONSUMER_KEY」などを別途jsonファイルなどの保存しておいて、そこから取得する形で記載していますが、
さくっとテストするだけなら、直接ハードコーディングしちゃってもいいと思います。

念のため記載しておくと、
CONSUMER_KEY、CONSUMER_SECRET、ACCESS_TOKEN、ACCESS_TOKEN_SECRET はぞれぞれ、「1. Tokenまわりを整える」でゲットした「API_KEY」、「API_KEY_SECRET」、「ACCESS_TOKEN」、「ACCESS_TOKEN_SECRET」になります。

conversion_timeもハードコーディングしていますが、
本来はコンバージョン発生した時刻を使ってください。
datetimeとか使えばいかようにもなるかと思います。

例えばこんな感じ

import datetime

datetime.datetime.now(tz=datetime.timezone.utc).isoformat(timespec='milliseconds').split('+')[0]+'Z'

# '2022-09-19T05:10:46.750Z'

Ⅳ. nodejsでリクエスト送信

nodejsでは、Pythonほど便利なモジュールはないですが、
OAuthモジュールを使用することができます。

例によってトークン系は別ファイルに記載して、そこから持ってきていますが、とりあえず動くか確認したい場合は直接記載してもいいと思います。

OAuthにconsumer_key(API_KEY)とconsumer_secret(API_KEY_SECRET)、及び、signature_method: 'HMAC-SHA1'、そして最後に実際に署名するための関数(hash_function)を渡して、インスタンス化します。

このhash_functionの中身をちゃんと理解しようとするとなかなか骨が折れますが、もともとOAuthの引数で渡している情報と、oauth.authorizeの引数で渡ってくる情報(アクセストークン系の情報)を処理して、ここでoauth_signatureを作成していることになります。

このoauth_signatureを生成するのに、HMACという認証方式を使用していて、その中のハッシュ関数に「SHA1」を用いているということ。
(HMACの計算をするために使用しているモジュールが「crypto」)

const axios = require('axios').default
const crypto = require('crypto');
const OAuth = require('oauth-1.0a');

const jsonFile = require('./tw_secret.json');

const oauth_token = jsonFile.ACCESS_TOKEN;
const oauth_token_secret = jsonFile.ACCESS_TOKEN_SECRET;
const consumer_key = jsonFile.API_KEY;
const consumer_secret = jsonFile.API_KEY_SECRET;

const capiUrl= "https://ads-api.twitter.com/11/measurement/conversions/hogehoge";

const oauth = OAuth({consumer: {
    key: consumer_key,
    secret: consumer_secret
  },
  signature_method: 'HMAC-SHA1',
  hash_function: (baseString, signingKey) =>{crypto.createHmac('sha1', signingKey).update(baseString).digest('base64')}
});

const token = {
    key: oauth_token,
    secret: oauth_token_secret
}

const authHeader = oauth.toHeader(oauth.authorize({
    url:capiUrl,
    method: 'POST'
},token))

const conversion_time = "2022-09-19T09:25:30.383Z";

const payload = {
    conversions:[
        {
            conversion_time: conversion_time,
            event_id: "tw-hogehoge-fugafuga",
            identifiers: [
                {
                    hashed_email: "48ddb93f0b30c475423fe177832912c5bcdce3cc72872f8051627967ef278e08"
                }
            ],
            value: "1000",
            price_currency: "JPY",
            number_item: 2,
            conversion_id: "abc2222",
            contents: [
                {
                    content_id: "ab34",
                    content_name: "T-Shirts",
                    content_type: "Apparel",
                    content_price: "500",
                    num_items: 2
                }
            ]
        }
    ]
}

axios.post(capiUrl, payload, {
    headers: {
        Authorization: authHeader["Authorization"],
    }
})
.then((res) => {
    console.log(res.data);
})
.catch((error) => {
    console.log(error.response.data);
})

ちなみに、

oauth.authorize({
    url:capiUrl,
    method: 'POST'
},token)

の戻り値は以下のようなオブジェクトになります。

{
  oauth_consumer_key: 'xvz1evFS4wEEPTGEFPHBog',
  oauth_nonce: 'kYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg',
  oauth_signature_method: 'HMAC-SHA1',
  oauth_timestamp: 1318622958,
  oauth_version: '1.0',
  oauth_token: '370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb',
  oauth_signature: 'tnnArxj06cWHq44gCs1OSK/jLY='
}

※この段階ではパーセントエンコードされていません。

これを「toHeader」すると、、つまり以下のようにすると

 oauth.toHeader(oauth.authorize({
    url:capiUrl,
    method: 'POST'
},token))

↓↓こんなオブジェクトが返ってきます。
(この時点でパーセントエンコードされます)

{
  Authorization: 'OAuth oauth_consumer_key="xvz1evFS4wEEPTGEFPHBog", oauth_nonce="kYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg", oauth_signature="tnnArxj06cWHq44gCs1OSKk%2FjLY%3D", oauth_signature_method="HMAC-SHA1", oauth_timestamp="1318622958", oauth_token="370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9we​​JAEb", oauth_version="1.0"'
}

この値がOAuth認証のヘッダーそのものなので、リクエストのときに用いればOKなわけです。

Ⅴ. GoogleAppsScript(GAS)でリクエスト送信

さて、いよいよGASの登場です。

Pythonとnodejsでは、oauth_signatureを生成してヘッダーを構築するところを、ある程度便利なライブラリやモジュールくんがやってくれていました。

キモとなるのは、nodejsでいうとこの部分。

  hash_function: (baseString, signingKey) => {
     crypto.createHmac('sha1', signingKey).update(baseString).digest('base64')
}

細かいところは一旦おいておいて、
signingKeyという「鍵」と、baseStringという「テキスト」を用いて
HMAC-SHA1認証の計算をして『oauth_signature』を求めているわけです。

HMAC-SHA1認証の計算をする部分』についてはGASでもメソッドが用意されています。

が、signingKeyとbaseStringは自力で作る必要があります。
ここが結構面倒です。

それを踏まえてコードを見ていきましょう。

function myFunction() {

  const tokenJson = {
      API_KEY: ScriptProperties.getProperty('API_KEY'),
      API_KEY_SECRET: ScriptProperties.getProperty('API_KEY_SECRET'),
      ACCESS_TOKEN: ScriptProperties.getProperty('ACCESS_TOKEN'),
      ACCESS_TOKEN_SECRET: ScriptProperties.getProperty('ACCESS_TOKEN_SECRET')
  };

const capiUrl= "https://ads-api.twitter.com/11/measurement/conversions/hogehoge";
const conversion_time = "2022-09-10T10:20:30.383Z";

const payload = {
    conversions:[
        {
            conversion_time: conversion_time,
            event_id: "tw-hogehoge-fugafuga",
            identifiers: [
                {
                    hashed_email: "48ddb93f0b30c475423fe177832912c5bcdce3cc72872f8051627967ef278e08"
                }
            ],
            value: '2000',
            price_currency: 'JPY',
            number_item: 2,
            conversion_id: 'abc123',
            contents: [
              {
                content_id: '12345',
                content_name: 'shoes',
                content_type: 'Apparel',
                content_price: '1000',
                num_items: 2
              }
            ]
        }
    ]
};


//↓↓↓ Authorizationを作る作業
let oauthParams = {
  oauth_consumer_key: tokenJson.API_KEY,
  oauth_nonce: (new Date().getTime()).toString(),
  oauth_signature: "",
  oauth_signature_method: "HMAC-SHA1",
  oauth_timestamp: Math.floor(new Date().getTime()/1000),
  oauth_token: tokenJson.ACCESS_TOKEN,
  oauth_version: "1.0"
}

let text="";
for(let k in oauthParams){
  if(k === "oauth_signature"){
    continue;
  }
  text += percentEncode(k) + "%3D" + percentEncode(oauthParams[k]) + "%26";
}
text = text.slice(0,-3);


const signingKey = percentEncode(tokenJson.API_KEY_SECRET) + '&' + percentEncode(tokenJson.ACCESS_TOKEN_SECRET);
const basestring = 'POST&' + percentEncode(getBaseUrl(capiUrl)) + '&' + text;

const oauth_signature = hash_func(basestring, signingKey);
oauthParams['oauth_signature'] = oauth_signature;


//↓↓ヘッダーの配列(の準備)
const headerParams = Object.keys(oauthParams).map((key) => {
  return key + '=' + oauthParams[key];
})

const headers = {
  'Authorization': 'OAuth ' + headerParams.join(', '),
  'Content-Type': 'application/json'
}

const option = {
  method: 'post',
  payload:JSON.stringify(payload),
  headers:headers ,
  escaping: false,
}

const response = UrlFetchApp.fetch(capiUrl, option);
console.log(response.getContentText());
}

function percentEncode(str) {
    return encodeURIComponent(str)
        .replace(/\!/g, "%21")
        .replace(/\*/g, "%2A")
        .replace(/\'/g, "%27")
        .replace(/\(/g, "%28")
        .replace(/\)/g, "%29");
}

function hash_func(basestring, key){
  const hmacSha1 = Utilities.computeHmacSignature(Utilities.MacAlgorithm.HMAC_SHA_1, basestring, key);
  const result = Utilities.base64Encode(hmacSha1);
  return percentEncode(result);
}

function getBaseUrl(url){
  return url.split('?')[0];
}

と、いう感じなのですが、順番に見ていきましょう。

まず、API_KEYなどはGASの機能であるスクリプトプロパティに格納しています。

GASの「プロジェクトの設定」をクリック。

そうすると、下の方に「スクリプトプロパティ」を追加できる欄があります。

ここにプロパティのキーと値を追加していきます。

ここで追加した値は
『ScriptProperties.getProperty(「プロパティ名を文字列で指定」)」
することで取得できます。

例えば、上のコードにもあるように

ScriptProperties.getProperty('API_KEY')

とすれば、プロパティ名「API_KEY」で登録した値が取得できるわけです。

payloadの部分は今までも説明してきたから大丈夫ですかね。
valueとcontent_priceは数値だとうまくいかないので、例によって文字列にしています。

以下では、ヘッダーを作るための準備段階として「oauthParams」というオブジェクトを定義しています。

let oauthParams = {
  oauth_consumer_key: tokenJson.API_KEY,
  oauth_nonce: (new Date().getTime()).toString(),
  oauth_signature: "",
  oauth_signature_method: "HMAC-SHA1",
  oauth_timestamp: Math.floor(new Date().getTime()/1000),
  oauth_token: tokenJson.ACCESS_TOKEN,
  oauth_version: "1.0"
}

キーの順番も最終的にアルファベット順にするので、この時点この順序にしておくといいと思います。

oauth_signature以外はこの時点で埋められますね。
oauth_consumer_keyは「API_KEY」ですし、oauth_tokenは「ACCESS_TOKEN」になります。

oauth_nonceはタイムスタンプでも問題ないため、そうしています。

oauth_signatureは後から追加してもいいのですが、
先に述べたように、順序も大事なので、この時点で「""(空文字)」で設定しておきましょう。

次は、HMAC-SHA1で使う、signingKeyとbaseStringを作っています。

const signingKey = percentEncode(tokenJson.API_KEY_SECRET) + '&' + percentEncode(tokenJson.ACCESS_TOKEN_SECRET);
const basestring = 'POST&' + percentEncode(getBaseUrl(capiUrl)) + '&' + text;

signingKeyは「API_KEY_SECRET」と「ACCESS_TOKEN_SECRET」をパーセントエンコードして、それらを「'&'」という文字列で繋いだものです。

パーセントエンコードについては下で関数として定義しています。

function percentEncode(str) {
    return encodeURIComponent(str)
        .replace(/\!/g, "%21")
        .replace(/\*/g, "%2A")
        .replace(/\'/g, "%27")
        .replace(/\(/g, "%28")
        .replace(/\)/g, "%29");
}

元々GASには「encodeURIComponent」という関数があり、基本的にはこれを使いますが、一部の文字はエンコードされないため、手動でリプレイスしています。

※一部の文字:「!」「*」「'」「(」「)」の5つですね。

そして、baseStringは「'POST&'」という文字列に
エンドポイントURL(今回はcapiURL)をパーセントエンコードして繋げ、さらに「'&'」を追加して、最後に「text」という変数を繋いでいます。

textを作っているのがこちらです。

let text="";
for(let k in oauthParams){
  if(k === "oauth_signature"){
    continue;
  }
  text += percentEncode(k) + "%3D" + percentEncode(oauthParams[k]) + "%26";
}
text = text.slice(0,-3);

要するに、oauthParamsのキーと値を「=」で結び、それを「&」で繋いでいく感じです。
そして全体をパーセントエンコードする。
(「=」をパーセントエンコードしたものが「%3D」、「&」をパーセントエンコードしたものが「%26」です)

上記の記述では最後にまとめてパーセントエンコードするのではなく
パーセントエンコードしたキーと値を「%3D」や「%26」で繋いでいますが
方法としてはなんでもいいかと思います。

このあたりはコードを見ればなんとなく分かるかもですが、
文章でごにょごにょ記載しても非常に分かりづらいため
ここまでoauth_signatureについての解説は避けていました。。。笑

そして、このsigningKeyとbaseStringを使って、HMAC-SHA1認証を計算しoauth_signatureを求めていきます。

ここについては以下の関数で定義しています。

function hash_func(basestring, key){
  const hmacSha1 = Utilities.computeHmacSignature(Utilities.MacAlgorithm.HMAC_SHA_1, basestring, key);
  const result = Utilities.base64Encode(hmacSha1);
  return percentEncode(result);
}

GASのUtilitiesの中にHMACの計算できるメソッドがあるため、それを利用します。

返り値をbase64でエンコードして、さらにパーセントエンコードしたものが「oauth_signature」ということです。

これを最初に定義したオブジェクトに代入しているのがこちらです。

const oauth_signature = hash_func(basestring, signingKey);
oauthParams['oauth_signature'] = oauth_signature;

これで「oauthParams」が完成したので、
ここからキーと値を繋いだ配列を作成しています。

const headerParams = Object.keys(oauthParams).map((key) => {
  return key + '=' + oauthParams[key];
})

実際にヘッダーとして使うときには、joinメソッドで「', '」で繋いで文字列にします。(さらに先頭に「'OAuth '」を加える)

'OAuth ' + headerParams.join(', ')

「', '」と、「,」の後ろに「 」半角スペースがありますが、
これはⅡ章の3-1のヘッダー構築でも記載したようにTwitter公式で以下のルールを設けているからです。

※実際は半角スペースなくても、エラーは出ません(が、念のため公式に従っています)

はい、これでGASでの記述も以上になります!

Pythonにしろ、nodejsにしろ、GASにしろ、上記のリクエストがうまく通れば、Twitterのイベントマネージャーの管理画面で「前回の記録日時」のところに記録されます。(もちろん実際コンバージョンとして計測するためには、キャンペーンとイベントを紐づける必要があります。)

また、リクエストがうまくいった場合は、レスポンスとして以下のようなコードが返ってきます。

{
  request: { params: { account_i: '1aaabbccc' } },
  data: {
    conversions_processed: 1,
    debug_id: '0dd98cc2-f3f2-aaaabbbbbccccc'
  }
}

「conversions_processed: 1」のところも要注意です。
payloadが間違っていると、エラーは出ないけど、「conversions_processed: 0」となっているときがたまにあります。このカウントが0では実際にはリクエストは上手く送れていません。

・・・

さて、すでにスーパーボリュームになってしまっていますので、
最初に宣言したとおり二部構成にさせていただきます。

ssGTMを活用した方法は後編に続きます。

それでは、Bye, bye.

この記事が気に入ったらサポートをしてみませんか?