【Google広告】拡張コンバージョンでメールアドレスを渡したときの内部挙動

広告運用をされている方々はよくご存知だと思いますが
Google広告には「拡張コンバージョン」という機能がありますね。
メールアドレスや電話番号などを渡してGoogle側でマッチさせコンバージョン計測を補完する目的のものです。
他の媒体でも「詳細マッチング」や「Advanced Matching」と呼ばれていたり、
API経由になると「コンバージョンAPI」と呼ばれることもあると思います。
今回はGoogle広告の「拡張コンバージョン」にメールアドレスを渡した時
内部処理でどのようなことをやってた上でGoogleサーバーへ飛ばしているのかというのを
コードベースに深堀りしていきたいという内容です。
目次
拡張コンバージョン概要
釈迦に説法とは思いますが
拡張コンバージョンとは?という話と歴史的背景のようなものを最初に記載します。
背景とかは知っている!特に興味ない!という方は次の章まで飛ばしてください。
拡張コンバージョンとは?
ひとことで言うと、「自社で集めた顧客データ(1st Party Data)を使って、より正確にコンバージョンを計測・補完する機能」のことです。
従来の計測では、サイトを訪れたユーザーのブラウザに「Cookie(クッキー)」という目印を付与して、その後の行動を追跡していました。一方、拡張コンバージョンでは、ユーザーが商品購入や会員登録の際に入力する「メールアドレス」や「電話番号」などの自社データを活用します。
このデータを安全な形でGoogleに送信し、Googleアカウントのログイン情報と「答え合わせ(照合)」をすることで、Cookieだけに頼らず「誰が広告経由でコンバージョンしたのか」を正確に把握できる仕組みです。
では、なぜわざわざこのような新しい計測方法が誕生したのでしょうか?
その理由は、Web計測を取り巻く「歴史的背景」と、それを補う「技術的背景」があります。
1. 歴史的背景:1st Party Cookieへの「寿命制限」と「デバイスの壁」
かつてWeb広告の計測はサードパーティCookieに依存していましたが、プライバシー保護の観点から姿を消しました。それに代わって現在のコンバージョン計測の主役となったのが、自社サイトで発行する「1st Party Cookie」です。
しかし近年、この1st Party Cookieにも厳しい制限がかけられるようになりました。
特に影響が大きいのが、AppleのSafariに搭載されている「ITP(Intelligent Tracking Prevention)」です。
ITPは、JavaScriptで付与された1st Party Cookieの有効期限を「最大7日間」や「24時間」へと強制的に短縮してしまいます。
つまり、広告をクリックして数日後に購入したユーザーのデータが、Cookieの消滅によって「計測漏れ」になってしまうのです。
さらに、Cookieはブラウザ単位で保存されるため、「スマホで広告を見て、後日PCで購入した」というようなクロスデバイスの動きも追跡できないという弱点があります。
2. 技術的背景:Cookieに頼らない「顧客データ×ハッシュ化」の照合
「すぐ消えてしまうCookie」や「デバイスの壁」をどう乗り越えればいいのか?
そこで登場した技術的なアプローチが、
先述したファーストパーティデータ(自社が保有する顧客データ)の直接活用です。
もちろん、ユーザーの個人情報をそのままGoogleに送るわけではありません。
「ハッシュ化」という不可逆の技術を使って、元のデータに戻せない安全な文字列に変換した上で送信します。
そして、Google側が抱える膨大な「ログインユーザーのデータ」と安全に照合を行います。
これにより、Cookieが消えてしまっていたり、デバイスをまたいだりしたコンバージョンであっても、
正確に「あの広告をクリックした〇〇さんだ」と紐付けることができるようになりました。
簡易まとめ
結論として、拡張コンバージョンが必要な理由は以下の2点に集約されます。
- ITPによるCookieの寿命制限や、クロスデバイスによる「計測漏れ」を防ぐため
- 正確なデータを取り戻すことで、Google広告の「自動入札(機械学習)」の精度を最大化するため
計測データが不正確になれば、AIによる自動入札も的外れになってしまいます。
プライバシーを強固に守りながら、広告のパフォーマンスを維持・向上させるために、拡張コンバージョンへの移行はもはや急務といえる状況になっているわけです。
メールアドレスについて各ドキュメントの記載
拡張コンバージョンでメールアドレスを渡す方法として
「生データ」を渡す方法と「(自分で)ハッシュ化した値」を渡す方法があります。
コードの話をする前に各ヘルプ・ドキュメントではどのような記載をしているか見てみましょう。
GoogleTagManagerを使う方法のヘルプ
GoogleTagManager(以下、GTM)の設定に関するドキュメントは以下です。(2026年4月現在)
Google タグ マネージャーを使って拡張コンバージョン(ウェブ向け)を設定する – Google 広告 ヘルプ
自分たちでハッシュ化する場合の手順は以下のように書かれています。
正規化の場合:
- 先頭や末尾の空白文字を削除する。
- テキストを小文字に変換する。
- 電話番号を E.164 規格の形式にする。
ハッシュ化の場合:
- 16 進数 SHA256 を使用する。

Googleタグを使う方法のヘルプ
Googleタグ(gtagのハードコード)設定に関するドキュメントは以下です。(2026年4月現在)
Google タグを使用して拡張コンバージョン(ウェブ向け)を設定する – Google 広告 ヘルプ
自分たちでハッシュ化する場合の手順は以下のように書かれています。
正規化の場合:
- 先頭や末尾の空白文字を削除する。
- テキストを小文字に変換する。
- 電話番号を E164 規格の形式にする。
- gmail.com と googlemail.com のメールアドレスのドメイン名の前にあるすべてのピリオド(.)を削除する。
ハッシュ化の場合:
- 16 進数 SHA256 を使用する。

GoogleAdsAPIを使う方法のヘルプ
GoogleAdsAPIでの設定に関するドキュメントは以下です。(2026年4月現在)
オンライン クリック コンバージョンを管理する | Google Ads API | Google for Developers
(メールアドレスを)ハッシュ化する手順について以下のような記載になっています。
拡張コンバージョンのメールアドレス形式
拡張コンバージョンのマッチ率を最大限に高めるには、ハッシュ化する前にメールアドレスを適切にフォーマットする必要があります。手順は次のとおりです。
- 標準の正規化を適用する(すべてのメールアドレス):
- 先頭または末尾の空白文字を削除します。
- メールアドレス全体を小文字に変換します。
- 該当する場合は、Gmail/Googlemail 固有の正規化を適用します。
- この手順は、ドメインが
gmail.comとgooglemail.comのメールアドレスにのみ適用されます。 - ピリオド(
.)を削除する: ユーザー名部分(@の前)から、すべてのピリオド文字を削除します。 - プラス接尾辞(
+)を削除する: ユーザー名部分から、プラス記号(+)とそれに続くすべての文字を削除します。- 例:
Jane.Doe+Shopping@googlemail.comは最初にjane.doe+shopping@googlemail.com(小文字)になり、次にjanedoe@googlemail.com(Gmail/Googlemail 固有のルール)になります。
- その他のドメイン:
gmail.comまたはgooglemail.com以外のドメインのメールアドレスでは、ユーザー名からピリオドやプラスの接尾辞を削除してはなりません。これらのアドレスには標準の正規化(小文字化、空白の削除)のみを適用します。- 例:
user.name+NYC@Example.comはuser.name+nyc@example.comに正規化されます。
- 正規化されたメールアドレスをハッシュ化します。
- 適切な正規化手順を適用した後、SHA-256 アルゴリズムを使用して結果のメールアドレスをハッシュ化します。

(参考)GA4のFirst-Paty dataのヘルプ
広告ではないですが、参考までにGA4のFirst-Party Dataに関するドキュメントは以下です。(2026年4月現在)
[GA4] ウェブサイトにコード スニペットを追加する(gtag.js) – アナリティクス ヘルプ
自分たちでハッシュ化する場合の手順は以下のように書かれています。
To hash the data:
- Use hex SHA256.
To normalize the data:
- Remove leading or trailing whitespaces.
- Convert the text to lowercase.
- Format phone numbers according to the E.164 standard.

なにか気付くことはあるでしょうか?
GoogleタグやGoogleAdsAPIのドキュメントでは
ドメインが「gmail.com」「googlemail.com」の場合は、「ドメイン名にある(@の前の部分)のピリオドを削除」と明記されています。
(例:ab.c.d@gmail.com ⇒ abcd@gmail.com にしてからハッシュ化する)
一方、GTMの方法ではこの記載がありません。
また、GoogleAdsAPIのドキュメントには、ドメインが「gmail.com」「googlemail.com」の場合のエイリアスに関する記述もあります。
少し横にそれますが gmail ではユーザー名の後に、+記号と任意の文字列を加えることができます。
別のアドレスやエイリアスからメールを送信する – Gmail ヘルプ
以下のような記述があります。
ユーザー名の後にカテゴリを追加すると、簡単にメールを分類できます。
たとえば、次のエイリアス宛に送信されたメールはすべて janedoe@gmail.com に届きます。
- janedoe+school@gmail.com
- janedoe+notes@gmail.com
- janedoe+important.emails@gmail.com
要するに、GoogleAdsAPIで書いてあることは次のようなことですね。
- janedoe+school@gmail.com
- janedoe+notes@gmail.com
- janedoe+important.emails@gmail.com
の3つはGoogle内部では同じメールアドレスとみなされます。
しかし、SHA-256でハッシュ化した際には、当然それぞれ異なる値になります。
つまり、異なるメールアドレスとして判定されてしまうため、
+ 記号とそれに続く文字はすべて削除した上でハッシュ化してね、といっています。
ドキュメントによって書いてあることが異なるのはどういうことなんでしょうか?
結論
先に結論を書いてしまいましょう。
自分たちでハッシュ化した値を渡すとき
「ドメインが gmail.com / googlemail.com の際にユーザー名部分のピリオドを削除」してからハッシュ化する、というのは必要工程です。
生データを渡したときは、内部でも上記の処理をしています(後ほどコードを見てみましょう)
一方、エイリアスの処理はどうでしょうか?
実は、生データを渡したとき、内部処理(内部で正規化/ハッシュ化する処理)でも
「エイリアスの + 記号以降を削除してからハッシュ化」ということはしていません。
つまり、GTMやGoogleタグの user_data に ab.cd@gmail.com を渡したときと、ab.cd+XXX@gmail.com を渡したときでは、別の値としてGoogleには認識される(Googleサーバーに送られる)というわけですね。
そのため、エイリアスが付与されている場合
「ユーザー名の + 記号以降を削除」してから渡す必要があります。
自分たちでハッシュ化してから渡す場合はもちろんですが
生データを渡す場合も、前述したように内部でもエイリアス削除の処理はしていないので
自分たちで処理してから渡す必要があります。
・・・こう思う人もいるかもしれません。
「あくまで、エイリアスがあっても同一人物として処理したい場合の話でしょ?
逆にエイリアスは別人として認識させたい場合はこの処理はしなくていいんじゃない?」
これは一部正しいですが、気をつけなくてはいけないポイントもあります。
例えば、abcd@gmail.com というアカウントがある場合、
確かにGoogleは abcd@gmail.com も abcd+X@gmail.com も abcd+XX@gmail.com も 同じユーザーと認識できます。
しかし拡張コンバージョンではGoogleに送られる前に必ずハッシュ化されます。
abcd@gmail.com をハッシュ化した値
と
abcd+X@gmail.com をハッシュ化した値
と
abcd+XX@gmail.com をハッシュ化した値
は当然異なります。
そしてハッシュ化は不可逆なので、「abcd+XX@gmail.com をハッシュ化した値」を見ても
そのハッシュ化前の値が「abcd+XX@gmail.com」だということは分からないわけです。
Googleの拡張コンバージョンの仕組みとしては、Googleアカウントにログインしている前提で、広告をクリックしたときにアカウント単位で情報を蓄積して、ユーザーがコンバージョンした際に、メールアドレスなどを渡すことで
Googleアカウントと照合して「このユーザーはこの広告をクリックしてコンバージョンしたんだな」と判定するわけです。
「Googleアカウントにログインしている前提」と書いたように、「アカウント単位」で見てます。
そのため、「ハッシュ化した値」という点でいうと、Googleが知っているのは「abcd@gmail.com をハッシュ化した値」だけです。
なのでハッシュ化した値を照合するときは「abcd@gmail.com をハッシュ化した値」しかマッチしません。
この点を踏まえると、「エイリアスがあっても同一人物として処理したい場合」に、+ 記号以降を削除してからハッシュ化が必要なのはそのとおりです。
一方、「逆にエイリアスは別人として認識させたい場合はこの処理はしなくていいんじゃない?」の部分はどうでしょうか。
これは正確ではありません。「別人として認識される」のではなく、「誰にもマッチしない」 という動作になります。
なぜかというと——
Google内部には abcd+XXX@gmail.com というアカウント自体が存在しません。
Googleが保持しているのはあくまで 実際のアカウントのメールアドレスをハッシュ化した値(= abcd@gmail.com のハッシュ)だけです。
つまり、照合(マッチ)の構造はこうなります:
送った値 | Google内部に存在するか | 結果 |
|---|---|---|
| ある | マッチ |
| ない | 誰にもマッチしない |
「別人扱いされる」には、Google側に abcd+XXX@gmail.com というアカウントが実在している必要がありますが、そうではないので、マッチ先がそもそも存在しないわけです。
そのため、「エイリアスが付与されている場合は誰にもマッチさせたくない」という意図がある場合のみ、+ 記号以降の削除処理はしなくていい、ということになります。
というわけで、エイリアス処理の話でした。
簡単にまとめると
エイリアスについては付与されているメールアドレスをどう扱うかによって処理が変わることもありますが
gmail.com/googlemail.comドメインの際のピリオド削除は必須工程になります。
GTMのドキュメントは、gmail.com/googlemail.com の時の記述が抜けているので、ちょっと優しくない書き方になっているかな〜と思います。
自分たちでハッシュ化した値を渡す場合は気をつけましょう。
※生データを渡す場合は内部でそこの処理をしてからハッシュ化してくれているので大丈夫です。
メールアドレスを渡したときの内部処理
メールアドレスを渡したときにどのような処理をするのか見ていきましょう。
これはGoogleタグの場合でもGTMの場合でも処理は同じです。
ざっくり以下の手順で処理が行われています。
生データを送る場合
例: { user_data: { email: 'Jane.Doe@Gmail.com' } }
- HTTPS チェック — http だったら処理を止めてエラーコードを返す
- 同意チェック —
ad_user_dataとad_storageの両方が granted でなければ処理しない - trim — 前後の空白を除去する(
" jane@gmail.com "→"jane@gmail.com") - 小文字化 — 全て小文字にする(
"Jane.Doe@Gmail.com"→"jane.doe@gmail.com") - gmail / googlemail のピリオド除去 — ドメインが
gmail.またはgooglemail.の場合、ユーザー名のピリオドを全部消す("jane.doe@gmail.com"→"janedoe@gmail.com") - バリデーション —
@で分割して形式チェック。 - SHA-256 ハッシュ —
crypto.subtle(非同期)または独自実装(同期)で SHA-256 を計算する - base64url エンコード — ハッシュのバイナリを
btoa()で base64 変換し、+→-、/→_、末尾の = を除去した 43 文字の文字列にする - 送信パラメータに組み込む —
tv.1~em.{43文字}の形式で Google のサーバーに送る
ハッシュ化済の値を送る場合は以下のような感じです。
例: user_data: { sha256_email_address: 'a3f2...(64文字hex)' }
- HTTPS チェック・同意チェック(上記と同じ)
- trim — 前後の空白を除去する
- 64文字 hex かチェック —
/^[0-9A-Fa-f]{64}$/にマッチするか確認する - マッチした → そのまま格納 — 正規化もハッシュもしない。値はそのまま使う
- 送信パラメータに組み込む —
tv.1~em.{64文字hex}の形式で送る
以降、実際にコードを見ていきましょう。
大きく分けて Phase0 から Phase3 の4段階あります。
HTTPSチェック・同意チェックについては割愛しています。
Phase0: trim
まず、メールアドレスに限らずすべての user_data 共通で trim処理が走ります。
以下のようなコードになっています。
function trimString(str) {
return str ? str.replace(/^\s+|\s+$/g, "") : "";
}Phase1: 正規化
このPhaseでは以下のような関数が走ります。
重要なところのみ抜き出してリライトしているので、実際のコードとは少々異なります。
const SHA256_FIELD_MAP = {
email: "sha256_email_address",
phone_number: "sha256_phone_number",
first_name: "sha256_first_name",
last_name: "sha256_last_name",
street: "sha256_street",
};
function getAndStore(userData, fieldName, normalizer, arrayIndex) {
const metadata = userData._tag_metadata || {};
let value = userData[fieldName]; // userData["email"] の値
let valueMeta = metadata[fieldName]; // そのフィールドのメタデータ
const sha256FieldName = SHA256_FIELD_MAP[fieldName]; // SHA256_FIELD_MAP["email"] = "sha256_email_address"
if (sha256FieldName) {
const sha256Value = userData[sha256FieldName];
const sha256Meta = metadata[sha256FieldName];
if (sha256Value) {
// sha256_* フィールドに値がある場合
value = sha256Value; // sha256_* を優先して上書き
valueMeta = sha256Meta;
}
}
if (arrayIndex !== undefined) {
// address 配列など、インデックスが指定されている場合は1件だけ処理
trimAndStore(value, fieldName, normalizer, arrayIndex, valueMeta);
} else {
// 単一値の場合も toArray() で統一して trimAndStore() を呼ぶ
const values = toArray(value);
const metas = toArray(valueMeta);
for (let i = 0; i < values.length; i++) {
trimAndStore(values[i], fieldName, normalizer, undefined, metas[i]);
}
}
}何をしているのかの前に1つだけ補足を。
Google広告の拡張コンバージョンではメールアドレスを渡すフィールドとしてemail というのと sha256_email_address というフィールドが存在します。

ヘルプには email には 生データ、sha256_email_address にはハッシュ化された値を入れる。
という風には書いてありますが、実は内部処理的にはどっちに何をいれても最終的に同じ処理をされます。
つまり、email に生データを渡しても ハッシュ化された値を渡しても 問題ないですしsha256_email_address に生データを渡しても ハッシュ化された値を渡しても 問題ないです。
email に生データを入れても sha256_email_address に生データを入れても最終的なアウトプットの値は同じです。
同様に、emailにハッシュ値を入れても、sha256_email_address にハッシュ値を入れても最終的なアウトプットの値は同じです。
じゃあどうしてフィールド分けてるんだ?
という話ですが、それはGoogleにしか分かりません。
(一応、内部的な警告が出るどうかは変わりますが、出力には違いはありません。)
とはいえ、せっかくフィールドが用意してあるので基本的には
生データの場合は email を使い、ハッシュ化された値の場合は sha256_email_address を使うのが良いと思います。
上記のコードはuser_data から email の値と sha256_email_address の値を取得し、
両方の値が存在する場合は sha256_email_address の値を優先する(上書きする)という処理をしています。
この時点では、値が「生データ」なのかとか「ハッシュ化済」なのかとかは全く見ていません。
また、前述の通りemail と sha256_email_address では後者のフィールドが優先されるので
仮に、email にハッシュ化済の値を入れて、sha256_email_addressに生データを入れたとしてもsha256_email_addressの値が後続の処理に使われてしまうので気をつけましょう。
さて、上記コードで最終的に呼ばれているのが trimAndStore です。trimAndStoreでは以下のようなことをしています。
function trimAndStore(value, fieldName, normalizer, arrayIndex, metadata) {
// ── Step 1: trim ─────
// 既に trim しているがここでも trim する
// null / 非文字列ガード付き trim。ハッシュ済みでも生データでも必ずここを通る。
const trimmed = safeStringTrim(value);
// trim 後に空文字になったら何もしない(値なし扱い)
if (trimmed === "") return;
// ── Step 2: 64-hex 判定(ハッシュ済みかどうかの分岐) ──
if (IS_PREHASHED.test(trimmed)) {
// ── ケース: ハッシュ済み(64-hex)──
// 正規化も SHA-256 もしない。trim しただけの 64-hex をそのまま list に積む。
if (metadata) metadata.isPreHashed = true;
const item = { name: fieldName, value: trimmed, index: arrayIndex };
if (metadata) item.metadata = metadata;
list.push(item);
} else {
// ── ケース: 生データ ──
// 正規化関数(後述:normalizeEmail)を通して正規化済みのテキストを積む。
// SHA-256 はまだかけない。Phase 2 で行う。
const normalized = normalizer(trimmed);
const item = { name: fieldName, value: normalized, index: arrayIndex };
if (metadata && normalized) {
// 診断用: 正規化前後の文字数を記録しておく
metadata.rawLength = String(trimmed).length;
metadata.normalizedLength = normalized.length;
item.metadata = metadata;
}
list.push(item);
}
}ここではじめて、渡ってきた値が すでにハッシュ化されている値なのか、生データなのか、という判定と分岐が行われます。
ハッシュ化済の場合は、この後の正規化やハッシュ化の処理をせずに、そのままの値が Phase3 で使われます。
生データの場合は、normalizer(正規化)の処理が走ることになります。
この記事では メールアドレスにフォーカスしているので、normalizerの関数名は normalizeEmail とします。
function normalizeEmail(trimmed) {
var parts = trimmed.toLowerCase().split("@");
// ↑ まず全文字を小文字化してから @ で分割
if (parts.length !== 2) return "e0";
// @ が 1 つでない(0個または2個以上)→ バリデーション失敗
var local = parts[0]; // ローカルパート(=ユーザー名)(@ より前)
var domain = parts[1]; // ドメインパート(@ より後)
// gmail.com / googlemail.com のみ、ローカルパート(ユーザー名)のピリオドを削除
// 例: "jane.doe@gmail.com" → local = "janedoe"
// 注意: "jane.doe@yahoo.com" はピリオドを削除しない
if (/^(gmail|googlemail)\./.test(domain)) {
local = local.replace(/\./g, "");
}
var result = local + "@" + domain;
// 最終的な形式チェック: @前後に非空白文字があり、ドメインに . が含まれるか
return /^\S+@\S+\.\S+$/.test(result) ? result : "e0";
}この正規化のタイミングで最初に記載した以下が実行されます。
そしていよいよ、Phase2へと進んでいきます。
Phase2: ハッシュ化とbase64
Phase2でハッシュ化及び、Bse64でのエンコードが行われます。
具体的には次のような処理が走ります。
function hashEmailAsync(value) {
// 空文字・正規化失敗:そのまま返す
if (value === "" || value === "e0") {
return Promise.resolve(value);
}
// すでに 64文字 hex(ハッシュ済み)ならそのまま返す(再ハッシュしない)
// → Phase 1 で 64-hex として格納されたものはここで止まる
if (/^[0-9A-Fa-f]{64}$/.test(value)) {
return Promise.resolve(value);
}
const utf8Bytes = encodeToUtf8Bytes(value); // encodeToUtf8Bytesについては後述
return window.crypto.subtle
.digest("SHA-256", utf8Bytes) // ArrayBuffer を返す Promise
.then(function(hashArrayBuffer) {
return toBase64url(hashArrayBuffer); // 43文字 base64url
})
.catch(function() {
return "e2"; // ハッシュ計算失敗
});
}ハッシュ化済の値を渡した場合はここでは何も処理が加えられずそのまま値が return されます。
生データの場合はここで、SHA-256のハッシュ化及び、base64のエンコードが実行されます。
encodeToUtf8Bytes は以下のように、単純にutf-8のバイト配列にする関数です。
function encodeToUtf8Bytes(str) {
try {
return new TextEncoder().encode(str); // 通常はこちら
} catch (e) {
return legacyStringToBytes(str); // TextEncoder 非対応環境のフォールバック
}
}
// 例: "abcd@gmail.com" → Uint8Array([97, 98, 99, 100, 64, 103, 109, ...])なぜバイト配列(=Uint8Array)にするかというと、crypto.subtle.digest の第2引数に入れるためです。(普通の文字列とかではNGのため)
というわけで、この部分を見ていきたいと思います。
window.crypto.subtle
.digest("SHA-256", utf8Bytes) // ArrayBuffer を返す Promise
.then(function(hashArrayBuffer) {
return toBase64url(hashArrayBuffer); // 43文字 base64url
})ここは特別な処理をしているというよりは
JavaScriptで SHA-256のアウトプットをbase64にしたい場合によくある処理をしています。
crypto.subtle.digest は Promiseを返しますので、then で受けています。then コールバック関数の引数に入る hashArrayBuffer は生のバイナリデータです(ArrayBufferと呼ばれるもの)
ArrayBufferはこのままでは加工が出来ないので toBase64url でさらに処理が続きます。
function toBase64url(hashBytesOrBuffer) { // ij()
// SHA-256 の出力(ArrayBuffer)を 0〜255 の整数配列に統一
const byteArray = Array.from(new Uint8Array(hashBytesOrBuffer));
// 各バイトを String.fromCharCode() で1文字に変換してつなぐ
// → btoa() が受け付ける「バイナリ文字列」を作るため
const binaryString = byteArray
.map(function(byte) { return String.fromCharCode(byte); })
.join("");
return window.btoa(binaryString) // 通常の base64(44文字、末尾に = あり)
.replace(/\+/g, "-") // base64url: + → -
.replace(/\//g, "_") // base64url: / → _
.replace(/=+$/, ""); // padding の = を除去 → 43文字に
}
// 例:
// SHA-256("abcd@gmail.com") のバイト列
// → btoa("...") → "8Fvs5WSRbCofuzie5G+32UoU40vMpKxeuX//GCaiADo="
// → replace → "8Fvs5WSRbCofuzie5G-32UoU40vMpKxeuX--GCaiADo"(43文字)まず ArrayBuffer に Uint8Array を噛ませて読み取れるようにして、さらに普通の配列(byteArray)にしています。
そして配列内の数値を1つずつ文字に変換して join することで バイナリ文字列を作成しています。
なぜこんなことをするかというと、btoa メソッドが引数としてバイナリ文字列しか受け付けないからですね。
仮に、btoa() に 配列 を直接渡すと 配列の中身がカンマ区切りになった文字列 が base64 化されるだけなので NGです。(JavaScriptが気を利かせて「配列を文字列に変換(toString)」してから処理しようとするため。)
ここでの記述は古いブラウザにも対応するために昔ながらの記述になっているのですが
もう少しスマートに書こうとしたら以下のような感じになるかと思います。
const str = "abcd@gmail.com";
async function hash() {
const utf8Bytes = new TextEncoder().encode(str);
const digest = await window.crypto.subtle.digest("SHA-256", utf8Bytes);
const u8arr = new Uint8Array(digest); // Uint8Array
// base64
const hashBase64 = btoa(String.fromCodePoint(...u8arr));
// url safe にする
// + 記号を - に、/ を _ に変更し、= を除去
const hashBase64Url = hashBase64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
return hashBase64Url;
}どうでしょうか?
多少モダンな形になって見やすくなったかもしれません(人に依るかもしれませんが)。
さて、元々の記述に戻ると、最後に以下を return しています。
return window.btoa(binaryString) // 通常の base64(44文字、末尾に = あり)
.replace(/\+/g, "-") // base64url: + → -
.replace(/\//g, "_") // base64url: / → _
.replace(/=+$/, ""); // padding の = を除去 → 43文字にwindow.btoa は base64にエンコードするためのメソッドです。
最後の replace 3連発は何をしているかというと、URLに安全に含められる(url safeな)文字セットにするために文字変換しています。
拡張コンバージョンの user_data の値などは、Googleの送信パラメータとして、URLのクエリストリングに含まれるため、url safe にする必要があります。
具体的には + を - に変換、 / を _ に変換、末尾の = はすべて削除、ということをしています。
具体的な値を見てみよう
次のコードを実行してみます。
const str = "abcd@gmail.com";
async function hash() {
const utf8Bytes = new TextEncoder().encode(str);
const digest = await window.crypto.subtle.digest("SHA-256", utf8Bytes);
const u8arr = new Uint8Array(digest); // Uint8Array
// base64
const hashBase64 = btoa(String.fromCodePoint(...u8arr));
// url safe にする
// + 記号を - に、/ を _ に変更し、= を除去
const hashBase64Url = hashBase64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
console.log(hashBase64Url);
}
hash()
この場合、8Fvs5WSRbCofuzie5G-32UoU40vMpKxeuX--GCaiADo という文字列が最終的に送られる値です。
余談ですが JavaScriptだと非常にごちゃごちゃと配列にしたり文字に変換したりしてややこしいですが
例えばPythonだと以下のようにシンプルに書けます。
import base64
import hashlib
s = "abcd@gmail.com"
hash = hashlib.sha256(s.encode()).digest()
base = base64.urlsafe_b64encode(hash).decode().rstrip("=")
print(base)
出力されている値は同様に 8Fvs5WSRbCofuzie5G-32UoU40vMpKxeuX--GCaiADo ですね。
実際にGoogle広告で設定してみる
実際にGoogle広告で user_data を設定してみましょう。
定数で abcd@gmail.com を準備

ユーザー提供データ 「ud_abcd@gmail.com」に上記をセッティング

Google広告コンバージョンタグのイベントパラメータに user_data をセットする。

Networkでリクエストを見ると、以下のようになっています。

「tv.1~em.」については Phase3の話なので一旦忘れてもらって、その後の値を見てください。
先ほど見たように、8Fvs5WSRbCofuzie5G-32UoU40vMpKxeuX--GCaiADo という文字列になっていますね。
Phase3: 送信パラメータに組み込む
これまで見てきたように、生データの内部処理の本質的なところは Phase2 までで終了していますので
Phase3 はさらっと流していきましょう。
非常に簡潔に書きますと以下のような処理をしています。
function(result) {
var parts = ["tv.1"];
list.forEach(function(item) {
const shortKey = getShortName[item.name] → "em" / "pn" / "fn" 等
// item.value はこの時点で SHA-256 + base64url 済み(または 64-hex のまま)
parts.push(shortKey + "." + item.value);
});
return { query: encodeURIComponent(parts.join("~")), ... };
// → "tv.1~em.8Fvs5WSR...~pn.xxxx~..."
};上記の関数が呼ばれる前の時点で、listは以下のような形です。
[
{ name: "email", value: "8Fvs5WSRbC..." }, // ← SHA-256 + base64url or SHA-256 + hex
{ name: "phone_number", value: "xxxxxxxxxxx..." }, // ← SHA-256 + base64url or SHA-256 + hex
{ name: "city", value: "osaka" },
]以下を実行すると、、、
list.forEach(function(item) {
const shortKey = getShortName[item.name] → "em" / "pn" / "fn" 等
// item.value はこの時点で SHA-256 + base64url 済み(または 64-hex のまま)
parts.push(shortKey + "." + item.value);
});parts配列は次のような感じになります。
["tv.1", "em.8Fvs5WSRbC...", "pn.xxxxxxxxxxx...", "ct.osaka"]これを、join("~") で結合して、encodeURIComponent することで最終的に
“tv.1~em.8Fvs5WSRbC…~pn.xxxxxxxxxxx…~ct.osaka” というような文字列ができるわけです。
メールアドレスしか渡していない場合は
tv.1~em.8Fvs5WSRbCofuzie5G-32UoU40vMpKxeuX–GCaiADo という値になるわけですね。
ちなみに、「tv.1」の部分は分かりやすくするためにハードコードしているわけではなく
実際に内部コード上もハードコードされています。なのでここは動的ではないんですね。
生データを渡す場合とハッシュ値を渡す場合
生データを渡す場合と、事前にハッシュ化した値を渡す場合では
最終的にGoogleに送られる値が異なることに気付くでしょうか?
今までの話の通り
生データを渡した場合、最終アウトプットは「tv.1~em.{43文字のurlsafeなbase64}」です。
一方、ハッシュ化された値を渡す場合、最終アウトプットは「tv.1~em.{64文字の hex}」です。
abcd@gmail.com を例にすると、生データを渡した場合は前述の通り
「tv.1~em.8Fvs5WSRbCofuzie5G-32UoU40vMpKxeuX–GCaiADo」です。
abcd@gmail.com を SHA-256でハッシュ化して 16進数(hex)でアウトプットすると「f05bece564916c2a1fbb389ee46fb7d94a14e34bcca4ac5eb97fbe1826a2003a」になるので
ハッシュ化した値を渡した場合の最終的なアウトプットは「tv.1~em.f05bece564916c2a1fbb389ee46fb7d94a14e34bcca4ac5eb97fbe1826a2003a」になります。
これは同じ abcd@gmail.com の SHA-256 値ですが、エンコード形式が異なるためです。
Googleサーバー側では両方のフォーマットを同一のアドレスとして解釈するので、この違いは問題ありません。
まとめ
いかがでしたでしょうか?
Google広告の拡張コンバージョンにおける user_data、 特にメールアドレスにフォーカスしてコードを追ってみました。
重要な点は以下かなと思います。
- 事前にハッシュ化した値を渡す際、ドメインが gmail.com / googlemail.com の場合は ユーザー名部分のピリオドは削除したものをハッシュ化する。
- ab.c.d@gmail.com ⇒ abcd@gmail.com にしてからハッシュ化する
- gmail.com / googlemail.com でエイリアスが付与されている場合は、生データ渡す場合でもハッシュ化した値を渡す場合でも、自分たちでエイリアス部分を削除する処理をする必要がある(別とみなしたいのであれば不要)
- abcd@gmail.com と abcd+XXX@gmail.com はGoogle内部では同一アドレスをみなされるが、ハッシュ化した値はそれぞれ異なってしまう
- abcd+XXX@gmail.comは +記号以降を削除して、abcd@gmail.com にしてから渡す(あるいはハッシュ化する)必要がある
- ※内部コードの処理でも abcd+XXX@gmail.com の +XXX を削除してくれる処理は走らないので、生データを渡す場合も、ここは自分たちで加工してから渡す必要がある
- 生データを渡した際の内部処理は、『trim → 小文字化 → SHA-256ハッシュ化 → base64エンコード → url safe変換』という過程になる
以上です。
何かの参考になれば幸いです〜。
Bye, bye.





