GA4のMeasurement Protocolで、LINEや電話で受けた予約と、その後の来館・成約をGA4へ送る実装。client_idの受け渡し、イベントの設計、api_secretの置き場所、72時間の制約、二重送信の防ぎ方、個人情報を送らない点検
LINEや
このclient_idをapi_secretは
この/debug/mp/collect)だけにmeasurement_idとapi_secretは
予約から
Measurement Protocolは、ブラウザの外で起きた出来事をサーバーからGA4へ届ける口
Measurement Protocolは、
送り先と
| 項目 | 内容 |
|---|---|
| 送り先 | https://www.google-analytics.com/mp/collect?measurement_id=G-…&api_secret=… |
| 必須の本文 | client_id(Webのeventsの配列 |
| よく |
user_id、timestamp_micros、validation_behavior |
| 1回に |
イベントは |
| 名前の |
イベント名・パラメータ名は_だけ |
| 値の長さ | パラメータの |
注意したいのは、
つなぐ鍵はclient_idで、予約を受けた時点で予約の記録に保存する
GA4のclient_idを、client_idを
予約の
| 予約の入口 | client_id | この |
|---|---|---|
| 自社サイトの |
フォームを |
予約の |
| LINEの |
ページに |
保存する。 |
| LINEの |
無い | GA4へは |
| 電話 | 無い | 同上 |
LINEのclient_idに
client_idがclient_idはclient_idが
ブラウザ側では、getでclient_idをgetの
// ga-ids.js — 予約フォームの送信前に、GA4 の client_id を隠し項目へ入れる
// タグが止められている・まだ読み込まれていないなどで値が返らなくても、予約の送信は止めない
function readGtagField(measurementId, field, timeoutMs = 1500) {
return new Promise((resolve) => {
if (typeof window.gtag !== 'function') return resolve('');
const timer = setTimeout(() => resolve(''), timeoutMs);
window.gtag('get', measurementId, field, (value) => {
clearTimeout(timer);
resolve(value == null ? '' : String(value));
});
});
}
document.querySelector('#booking-form').addEventListener('submit', async (e) => {
const form = e.currentTarget;
if (form.dataset.ready === '1') return; // 2回目の submit はそのまま送る
e.preventDefault();
form.elements.ga_client_id.value = await readGtagField('G-XXXXXXXXXX', 'client_id');
form.dataset.ready = '1';
form.requestSubmit();
});フォームの<input type="hidden" name="ga_client_id">をga_client_id列に
gtagなし: ""
gtagあり: "1838291045.1759550000"
応答なし: "" 1502msuser_idも
イベント名はGA4の推奨イベントのリード用を使い、パラメータは2つに絞る
予約から
| 予約システムの |
送る |
推奨イベントの |
|---|---|---|
| LINE・電話などで |
generate_lead |
フォームなどで |
| 来館・ |
working_lead |
利用者が |
| 成約した | close_convert_lead |
見込み客が |
| 成約に |
close_unconvert_lead |
顧客に |
Webのgenerate_leadをgenerate_leadを
パラメータはlead_source(予約のweb・line・phone)とbooking_ref(予約番号)のlead_sourceは
金額(valueとcurrency)も
api_secretはサーバーの環境変数だけに置き、ログにも残さない
api_secretは、
このGA4_MEASUREMENT_IDとGA4_API_SECRETをapi_secretが
鍵が
72時間より古い出来事は、送る側で先に止める
Measurement Protocolでは、timestamp_microsでvalidation_behaviorで
| validation_behavior | 72時間より |
|---|---|
指定なし・RELAXED(既定) |
受け付けるが、 |
ENFORCE_RECOMMENDATIONS |
受け付けない |
既定のENFORCE_RECOMMENDATIONSをvalidation_behaviorをENFORCE_RECOMMENDATIONSをENFORCE_RECOMMENDATIONSでは、
検証サーバーにENFORCE_RECOMMENDATIONSではVALUE_INVALIDが
もう
もう1つ、timestamp_microsはDate.now()はVALUE_INVALIDが
二重送信は、送る予定の表に「予約×段階」の一意制約をかけて防ぐ
同じga4_outbox)に(booking_id, stage)の
GA4のpurchaseイベントをpurchaseで、
送る
- 積む:
INSERT … ON CONFLICT (booking_id, stage) DO NOTHING。積み済みなら 何も 起きない - 取る:
UPDATE … SET status = 'sending' WHERE status = 'pending'。1行更新できた ときだけ送る。 送信の 処理が 同時に 2つ動いても、 片方は 0行で 終わる - 終える:結果を
sent・skipped・failedのどれかで 書く
通信がpendingに戻さずfailedでfailedの
個人情報は、送る項目を絞ったうえで、送る直前にも調べる
GA4には、
守り方のclient_id、仮名のuser_id、lead_source、booking_refだけで、
そのうえで、skippedにします。user_idにvalidationMessagesは
正規表現は000123のようなbk_7f3a9cのように
"1838291045.1759550000" []
"402918377.1759552222" []
"bk_7f3a9c" []
"090-1234-5678" [ 'phone' ]
"06 6123 4567" [ 'phone' ]
"+81 90 1234 5678" [ 'phone' ]
"[email protected]" [ 'email' ]
"000123" [ 'phone' ]検証サーバーで確かめたこと:形は見るが、鍵と個人情報は見ない
検証サーバーhttps://www.google-analytics.com/debug/mp/collect)は、validationMessagesでapi_secretがmeasurement_idと仮のapi_secretで
手元の
1. 正しい形 → HTTP 200
(validationMessages は空)
2. 80時間前・RELAXED(既定) → HTTP 200
(validationMessages は空)
3. 80時間前・ENFORCE_RECOMMENDATIONS → HTTP 200
VALUE_INVALID timestamp_micros: Measurement timestamp_micros has timestamp too far in the past. Measurement protocol only supports timestamps [72h] into the past. The current time in UNIX microseconds is [1791109866168959].
4. ミリ秒のまま入れた → HTTP 200
VALUE_INVALID timestamp_micros: Measurement timestamp_micros has timestamp too far in the past. Measurement protocol only supports timestamps [72h] into the past. The current time in UNIX microseconds is [1791109866328252].
5. 予約済みの名前 → HTTP 200
NAME_RESERVED events: Event at index: [0] has name [session_start] which is reserved.
6. 値が101文字・ENFORCE → HTTP 200
VALUE_TOO_LONG events.params: Param [note] has value [あああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああ] longer than max characters [100].
7. メールと電話をパラメータに入れた → HTTP 200
(validationMessages は空)読み取れる
- 既定の
動きでは、 2の古い 時刻も 通る。 80時間前は、 メッセージ無しで 通った。 止めたいなら ENFORCE_RECOMMENDATIONSを付けるか、 送る 側で 止める - ミリ秒の
入れ間違いは、 4はENFORCE_RECOMMENDATIONSなら 見つかる。 1970年の 時刻と して 「過去すぎる」と 返った - 値の
長さの 検証の誤りは VALUE_TOO_LONGという 種類で 返った。 ドキュメントに ある 種類の 一覧 ( VALUE_INVALIDなど)には、この 名前は 載っていなかった (意味の 近い VALUE_OUT_OF_BOUNDSは載っている)。 種類の 名前で 分けて 処理する 作りに するなら、 一覧に 無い 名前も 来る 前提に しておく - 個人情報は
止まらない。 7はメールアドレスと 電話番号を 含んでいても 空で 返った
validationMessagesが
積む・点検する・送るを通しで動かした結果
予約5件と、
| 予約番号 | 入口 | 出来事 | ねらい |
|---|---|---|---|
bk_7f3a9c |
Web | 2時間前に |
二重に |
bk_c41e02 |
LINE | 5時間前に |
ブラウザの |
bk_9d0b7e |
電話 | 1時間前に |
client_idが |
bk_e5a113 |
Web | 80時間前に |
72時間を |
[email protected] |
Web | 1時間前に |
予約番号に |
結果は
積む: true
同じ来館をもう一度積む: false
[
{
"id": "bk_e5a113",
"stage": "contracted",
"status": "skipped",
"problems": [
"発生から80時間たっている(72時間の上限に近いか超えている)"
]
},
{
"id": "bk_c41e02",
"stage": "booked",
"status": "sent",
"http": 200,
"validationMessages": []
},
{
"id": "bk_7f3a9c",
"stage": "visited",
"status": "sent",
"http": 200,
"validationMessages": []
},
{
"id": "bk_9d0b7e",
"stage": "visited",
"status": "skipped",
"problems": [
"client_id がない(Webの訪問とつなぐ鍵がないので送らない)"
]
},
{
"id": "[email protected]",
"stage": "visited",
"status": "skipped",
"problems": [
"個人情報らしい値: booking_ref(email)"
]
}
]
2回目の flush: []
┌─────────┬────────────────────┬──────────────┬───────────┐
│ (index) │ booking_id │ stage │ status │
├─────────┼────────────────────┼──────────────┼───────────┤
│ 0 │ 'bk_7f3a9c' │ 'visited' │ 'sent' │
│ 1 │ 'bk_c41e02' │ 'booked' │ 'sent' │
│ 2 │ 'bk_9d0b7e' │ 'visited' │ 'skipped' │
│ 3 │ 'bk_e5a113' │ 'contracted' │ 'skipped' │
│ 4 │ '[email protected]' │ 'visited' │ 'skipped' │
└─────────┴────────────────────┴──────────────┴───────────┘送ったのはvalidationMessagesはskippedにbk_7f3a9cは、enqueueがfalseを
コード全体
点検とfetchで
// ga4-mp.mjs — 予約の後に起きた出来事(来館・成約など)を、Measurement Protocol で GA4 へ送る
// サーバー側だけで動かす。api_secret は環境変数から読み、ブラウザへは絶対に渡さない。
const ENDPOINT = {
prod: 'https://www.google-analytics.com/mp/collect',
debug: 'https://www.google-analytics.com/debug/mp/collect', // 検証用。レポートには載らない
};
// 予約システムの出来事 → GA4 の推奨イベント(リード用)
export const EVENT_MAP = {
booked: 'generate_lead', // 予約を受けた(LINE・電話など、ブラウザの外で受けたとき)
visited: 'working_lead', // 来館・来店して担当者と会った
contracted: 'close_convert_lead', // 成約
lost: 'close_unconvert_lead', // 成約に至らなかった
};
const MAX_BACKDATE_MS = 72 * 60 * 60 * 1000; // ドキュメント上の上限
const SAFETY_MARGIN_MS = 60 * 60 * 1000; // 送信の遅れを見込んで1時間手前で打ち切る
const NAME_RE = /^[A-Za-z][A-Za-z0-9_]{0,39}$/; // 英字で始まり、英数字と_、40文字以内
const RESERVED_PREFIX = /^(_|firebase_|ga_|google_|gtag\.)/;
// 個人情報らしい値を送らないための最後の網(これだけに頼らず、送る項目を最初から絞る)
const PII_PATTERNS = [
{ kind: 'email', re: /[^\s@]+@[^\s@]+\.[^\s@]+/ },
{ kind: 'phone', re: /(?<!\d)(?:\+81[-\s]?|0)\d{1,4}[-\s]?\d{1,4}[-\s]?\d{3,4}(?!\d)/ },
];
export function findPii(value) {
const s = String(value);
return PII_PATTERNS.filter((p) => p.re.test(s)).map((p) => p.kind);
}
/**
* 送る前の点検。問題があれば理由の配列を返す(空なら送ってよい)
* @param {{clientId:string, userId?:string, name:string, params:object, occurredAt:Date}} ev
*/
export function checkEvent(ev, now = new Date()) {
const errors = [];
if (!ev.clientId) errors.push('client_id がない(Webの訪問とつなぐ鍵がないので送らない)');
if (!NAME_RE.test(ev.name)) errors.push(`イベント名が規則に合わない: ${ev.name}`);
const age = now - ev.occurredAt;
if (age > MAX_BACKDATE_MS - SAFETY_MARGIN_MS) errors.push(`発生から${Math.floor(age / 3600000)}時間たっている(72時間の上限に近いか超えている)`);
if (age < -5 * 60 * 1000) errors.push('発生時刻が未来になっている(時計か入力の誤り)');
const entries = Object.entries(ev.params ?? {});
if (entries.length > 25) errors.push('パラメータが25個を超えている');
for (const [k, v] of entries) {
if (!NAME_RE.test(k) || RESERVED_PREFIX.test(k)) errors.push(`パラメータ名が使えない: ${k}`);
if (typeof v === 'string' && v.length > 100) errors.push(`値が100文字を超えている: ${k}`);
const pii = findPii(v);
if (pii.length) errors.push(`個人情報らしい値: ${k}(${pii.join(',')})`);
}
for (const [k, v] of [['client_id', ev.clientId], ['user_id', ev.userId]]) {
if (v && findPii(v).length) errors.push(`${k} に個人情報らしい値`);
}
return errors;
}
/** Measurement Protocol の本文を組み立てる */
export function buildBody(ev) {
const body = {
client_id: ev.clientId,
timestamp_micros: ev.occurredAt.getTime() * 1000, // マイクロ秒(ミリ秒ではない)
validation_behavior: 'ENFORCE_RECOMMENDATIONS', // 72時間より前などは黙って直さず拒否させる
events: [{ name: ev.name, params: { ...ev.params } }],
};
if (ev.userId) body.user_id = ev.userId;
return body;
}
/** 送る。debug=true なら検証サーバーへ送り、validationMessages を返す */
export async function send(ev, { debug = false, env = process.env, fetchImpl = fetch } = {}) {
const { GA4_MEASUREMENT_ID: mid, GA4_API_SECRET: secret } = env;
if (!mid || !secret) throw new Error('GA4_MEASUREMENT_ID と GA4_API_SECRET をサーバーの環境変数に置く');
const url = `${debug ? ENDPOINT.debug : ENDPOINT.prod}?measurement_id=${encodeURIComponent(mid)}&api_secret=${encodeURIComponent(secret)}`;
const res = await fetchImpl(url, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(buildBody(ev)),
});
// 本番の /mp/collect は、中身が壊れていてもエラーのコードを返さない。確かめられるのは「届いたか」だけ
if (!debug) return { status: res.status };
return { status: res.status, ...(await res.json()) };
}送るnode:sqliteで
// outbox.mjs — 送る予定のイベントを DB に積み、1回だけ送るための仕組み(SQLite で確認。PostgreSQL でも同じ考え方)
import { DatabaseSync } from 'node:sqlite';
import { EVENT_MAP, checkEvent, send } from './ga4-mp.mjs';
export function openDb(path = ':memory:') {
const db = new DatabaseSync(path);
db.exec(`
CREATE TABLE IF NOT EXISTS bookings (
id TEXT PRIMARY KEY, -- 予約番号(数字だけにしない。例 bk_7f3a9c)
channel TEXT NOT NULL, -- web / line / phone
ga_client_id TEXT, -- 予約の時点で保存した GA4 の client_id(無ければ NULL)
member_key TEXT, -- user_id に使う会員の仮名ID(氏名・電話・メールから作らない)
created_at TEXT NOT NULL -- UTC の ISO 形式
);
CREATE TABLE IF NOT EXISTS ga4_outbox (
booking_id TEXT NOT NULL REFERENCES bookings(id),
stage TEXT NOT NULL, -- booked / visited / contracted / lost
occurred_at TEXT NOT NULL, -- 出来事が起きた時刻(UTC)
status TEXT NOT NULL DEFAULT 'pending', -- pending / sending / sent / skipped / failed
note TEXT,
updated_at TEXT,
UNIQUE (booking_id, stage) -- 同じ予約の同じ段階は1行だけ=二重送信の元を作らない
);
`);
return db;
}
/** 管理画面で「来館」などを記録したときに呼ぶ。2回押されても行は増えない */
export function enqueue(db, bookingId, stage, occurredAt) {
const r = db.prepare(
`INSERT INTO ga4_outbox (booking_id, stage, occurred_at) VALUES (?, ?, ?)
ON CONFLICT (booking_id, stage) DO NOTHING`,
).run(bookingId, stage, occurredAt.toISOString());
return r.changes === 1; // false なら積み済み
}
/** 送る前に「送信中」へ変える。別の処理が先に取っていたら false */
function claim(db, bookingId, stage, now) {
const r = db.prepare(
`UPDATE ga4_outbox SET status = 'sending', updated_at = ?
WHERE booking_id = ? AND stage = ? AND status = 'pending'`,
).run(now.toISOString(), bookingId, stage);
return r.changes === 1;
}
function finish(db, bookingId, stage, status, note, now) {
db.prepare(`UPDATE ga4_outbox SET status = ?, note = ?, updated_at = ? WHERE booking_id = ? AND stage = ?`)
.run(status, note, now.toISOString(), bookingId, stage);
}
/** 数分おきに動かす送信の処理 */
export async function flush(db, { now = new Date(), debug = false, env, fetchImpl } = {}) {
const rows = db.prepare(
`SELECT o.booking_id, o.stage, o.occurred_at, b.channel, b.ga_client_id, b.member_key
FROM ga4_outbox o JOIN bookings b ON b.id = o.booking_id
WHERE o.status = 'pending' ORDER BY o.occurred_at`,
).all();
const results = [];
for (const r of rows) {
if (!claim(db, r.booking_id, r.stage, now)) continue;
const ev = {
clientId: r.ga_client_id,
userId: r.member_key ?? undefined,
name: EVENT_MAP[r.stage],
occurredAt: new Date(r.occurred_at),
params: { lead_source: r.channel, booking_ref: r.booking_id },
};
const problems = checkEvent(ev, now);
if (problems.length) {
finish(db, r.booking_id, r.stage, 'skipped', problems.join(' / '), now);
results.push({ id: r.booking_id, stage: r.stage, status: 'skipped', problems });
continue;
}
try {
const res = await send(ev, { debug, env, fetchImpl });
const msgs = res.validationMessages ?? [];
const status = res.status >= 200 && res.status < 300 && msgs.length === 0 ? 'sent' : 'failed';
finish(db, r.booking_id, r.stage, status, msgs.map((m) => m.description).join(' / ') || null, now);
results.push({ id: r.booking_id, stage: r.stage, status, http: res.status, validationMessages: msgs });
} catch (e) {
// 届いたか分からないときは pending に戻さず failed で止め、人が確かめてから戻す
finish(db, r.booking_id, r.stage, 'failed', String(e.message), now);
results.push({ id: r.booking_id, stage: r.stage, status: 'failed', error: e.message });
}
}
return results;
}通しでmeasurement_idとapi_secretはdebug: trueなので
// run-debug.mjs — 検証サーバー(/debug/mp/collect)だけを相手に、積む→点検→送るを通しで動かす
// 本番の /mp/collect には送らない。measurement_id と api_secret は仮の値
import { openDb, enqueue, flush } from './outbox.mjs';
const env = { GA4_MEASUREMENT_ID: 'G-XXXXXXXXXX', GA4_API_SECRET: 'placeholder-secret' };
const now = new Date();
const hoursAgo = (h) => new Date(now.getTime() - h * 3600_000);
const db = openDb();
const addBooking = db.prepare('INSERT INTO bookings VALUES (?, ?, ?, ?, ?)');
addBooking.run('bk_7f3a9c', 'web', '1838291045.1759550000', 'm_2b8e41d0', hoursAgo(30).toISOString());
addBooking.run('bk_c41e02', 'line', '402918377.1759552222', null, hoursAgo(5).toISOString());
addBooking.run('bk_9d0b7e', 'phone', null, null, hoursAgo(3).toISOString());
addBooking.run('bk_e5a113', 'web', '771023456.1759100000', null, hoursAgo(120).toISOString());
addBooking.run('[email protected]', 'web', '55501234.1759553333', null, hoursAgo(2).toISOString()); // 悪い例:予約番号にメール
console.log('積む:', enqueue(db, 'bk_7f3a9c', 'visited', hoursAgo(2)));
console.log('同じ来館をもう一度積む:', enqueue(db, 'bk_7f3a9c', 'visited', hoursAgo(2)));
enqueue(db, 'bk_c41e02', 'booked', hoursAgo(5));
enqueue(db, 'bk_9d0b7e', 'visited', hoursAgo(1));
enqueue(db, 'bk_e5a113', 'contracted', hoursAgo(80)); // システムが止まっていて80時間前の成約が残っていた
enqueue(db, '[email protected]', 'visited', hoursAgo(1));
const results = await flush(db, { now, debug: true, env });
console.log(JSON.stringify(results, null, 2));
console.log('2回目の flush:', JSON.stringify(await flush(db, { now, debug: true, env })));
console.table(db.prepare('SELECT booking_id, stage, status FROM ga4_outbox').all());検証サーバーの
// probe-debug.mjs — 手元の点検を通さずに、検証サーバーが何を見て何を見ないかを確かめる
const URL_ = 'https://www.google-analytics.com/debug/mp/collect?measurement_id=G-XXXXXXXXXX&api_secret=placeholder-secret';
const nowMs = Date.now();
const base = (ev, extra = {}) => ({ client_id: '1838291045.1759550000', ...extra, events: [ev] });
const cases = {
'1. 正しい形': base({ name: 'working_lead', params: { lead_source: 'web', engagement_time_msec: 1 } }),
'2. 80時間前・RELAXED(既定)': base({ name: 'working_lead' }, { timestamp_micros: (nowMs - 80 * 3600e3) * 1000 }),
'3. 80時間前・ENFORCE_RECOMMENDATIONS': base({ name: 'working_lead' }, { timestamp_micros: (nowMs - 80 * 3600e3) * 1000, validation_behavior: 'ENFORCE_RECOMMENDATIONS' }),
'4. ミリ秒のまま入れた': base({ name: 'working_lead' }, { timestamp_micros: nowMs, validation_behavior: 'ENFORCE_RECOMMENDATIONS' }),
'5. 予約済みの名前': base({ name: 'session_start' }),
'6. 値が101文字・ENFORCE': base({ name: 'working_lead', params: { note: 'あ'.repeat(101) } }, { validation_behavior: 'ENFORCE_RECOMMENDATIONS' }),
'7. メールと電話をパラメータに入れた': base({ name: 'working_lead', params: { email: '[email protected]', tel: '090-1234-5678' } }, { user_id: '[email protected]' }),
};
for (const [label, body] of Object.entries(cases)) {
const res = await fetch(URL_, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) });
const j = await res.json();
console.log(`${label} → HTTP ${res.status}`);
for (const m of j.validationMessages) console.log(` ${m.validationCode} ${m.fieldPath}: ${m.description}`);
if (!j.validationMessages.length) console.log(' (validationMessages は空)');
}本番に向ける前に決めておくこと
コードを
- 送る
処理を :48時間・72時間の動かす間隔 制約が あるので、 1日1回ではなく、 数分〜数十分 おきに 動かす skippedとfailedを誰が :件数が見るか 増えたら 気づける 場所 (管理画面や 通知)に 出す - カスタムディメンションの
登録 :lead_sourceとbooking_refを、GA4の 管理画面で 登録する - プライバシーポリシーの
文面 :アクセス解析の識別子を 予約の 記録に 保存し、 来館・成約の 分析に 使う ことを 書いているか - 同意の扱い:同意を
得てから 計測する 作りの サイトなら、 同意が 無い人の client_idは空で 届くので、 その 予約は 送らない
このような
動作確認した環境
確認日は
| 項目 | バージョン・内容 |
|---|---|
| OS | macOS 26 |
| Node.js | 22.23.2fetchとnode:sqlite。 |
| 送り先 | https://www.google-analytics.com/debug/mp/collect(検証サーバー)のみ |
| measurement_id / api_secret | 仮の値(G-XXXXXXXXXX / placeholder-secret) |
| 確かめた |
ブラウザ側のclient_idの |
確かめていない/mp/collect)へはapi_secretでga-ids.jsは、
参照した公式ドキュメント(確認日:2026年10月4日)
- Google「Send Measurement Protocol events to Google Analytics」:送り先と
必須の 項目、 Measurement Protocolに 廃止の 予定は なく、 新しい サーバー間の 連携には Data Manager APIを 勧めている こと、 api_secretの作り方と クライアント側に 出さない こと、 72時間の 上限と validation_behavior、48時間以内に 届く 必要、 件数と 名前・値の 長さの 上限、 本番の 送り先が エラーコードを 返さない こと - Google「Validate Measurement Protocol events」:検証サーバーの
送り先、 validationMessagesの形と 種類の 一覧、 レポートに 載らない こと、 api_secretを調べない こと - Google「Measurement Protocol reference」:本文の
各項目 ( user_id・timestamp_microsはマイクロ秒・ validation_behavior)、送る ときは validation_behaviorを指定しないのが 推奨で、 厳しさを 優先するなら ENFORCE_RECOMMENDATIONSを付ける こと、 使えない イベント名と パラメータ名の 先頭 - Google「Recommended events」:
generate_lead・working_lead・close_convert_lead・close_unconvert_leadの意味とlead_source - Google「gtag.js API reference」:
getコマンドでclient_idを取り出す方法 - アナリティクス ヘルプ「Measure activity across platforms with User-ID」:user_idに
本人を 特定できる 情報を 含めない こと、 256文字以内、 1人に 1つの 変わらない ID - アナリティクス ヘルプ「Best practices to avoid sending Personally Identifiable Information (PII)」:URL・ページの
タイトル・User-ID・カスタムディメンション・利用者の 入力を 含めて 個人情報を 送らない こと - アナリティクス ヘルプ「Minimize duplicate key events with transaction IDs」:取引IDに
よる 重複の 除外が purchaseイベントの仕組みである こと
よくある質問
Measurement Protocolで送ったイベントが間違っていたら、エラーが返ってきますか?
本番の
電話で受けた予約も、GA4へ送ったほうがよいですか?
Webの
3日以上前の来館や成約を、あとからまとめて送れますか?
そのままの
user_idに、メールアドレスをハッシュにした値を使ってもよいですか?
この