0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

外部リンクの流入元がGA4に出ないとき:noreferrerとUTMをローカルで検証する

0
Posted at

外部の記事にリンクを掲載しても、GA4で掲載元の名前が見つからないことがあります。そこで「クリックされていない」と結論を出す前に、HTTPのRefererと、URLに付けるUTMパラメータを分けて確認します。この記事では、外部サービスやGA4にアクセスせず、Node.jsとブラウザだけで違いを再現します。

開示:筆者はImyaとTryonrの運営に関わっています。本文とコードの作成にはAIを利用しました。以下の結果は独立したローカル検証のもので、これらのサービスの内部実装・利用者データ・売上を示すものではありません。

参照元の情報を伏せてもURLのUTMラベルは別に残ることを表すイラスト

AI生成の概念イラスト。実際の製品画面や計測結果ではありません。

1. RefererとUTMは、入っている場所が違う

Refererはブラウザがリクエストに付けるHTTPヘッダーです。リンク元の設定やブラウザの方針によって、省略されたり、送信内容が制限されたりします。

一方、UTMは遷移先URLのクエリパラメータです。

https://example.com/tool?utm_source=qiita&utm_medium=referral&utm_campaign=link_demo

rel="noreferrer"は参照元情報を送らないための指定であり、リンク先URLのUTMを削除する指定ではありません。参照元が空でも、URLにUTMが残るケースは成立します。MDNのnoreferrerの説明を参照してください。

リンク元が分かることと、リンク元の記事のパスまで分かることも別です。strict-origin-when-cross-originでは、同じセキュリティレベルのクロスオリジン遷移で送られるのは原則としてオリジンだけです。媒体単位の識別と記事単位の識別を同じものとして扱わないようにします。

2. Node.jsで、ポートの違う2つのオリジンを用意する

以下をdemo.mjsとして保存し、node demo.mjsで実行します。追加パッケージは不要です。検証環境はNode.js 22.16.0とChromiumベースのブラウザです。

サーバーは127.0.0.1にのみバインドします。送信元と受信先でポートを変え、受信したヘッダーとクエリをそのまま表示します。GA4タグ、Cookie、アクセスログの保存は使いません。

demo.mjs
import http from 'node:http';

function campaignUrl(destination, content) {
  const url = new URL(destination);
  if (!['https:', 'http:'].includes(url.protocol)) {
    throw new TypeError('Expected an HTTP(S) URL');
  }
  url.searchParams.set('utm_source', 'qiita');
  url.searchParams.set('utm_medium', 'referral');
  url.searchParams.set('utm_campaign', '20261019_link_observability');
  url.searchParams.set('utm_content', content);
  return url.href;
}

const destination = http.createServer((req, res) => {
  const url = new URL(req.url, 'http://127.0.0.1');
  res.writeHead(200, {
    'Content-Type': 'text/plain; charset=utf-8',
    'Cache-Control': 'no-store',
  });
  res.end(JSON.stringify({
    referer: req.headers.referer ?? null,
    utm_source: url.searchParams.get('utm_source'),
    utm_medium: url.searchParams.get('utm_medium'),
    utm_campaign: url.searchParams.get('utm_campaign'),
  }, null, 2));
});

destination.listen(0, '127.0.0.1', () => {
  const target = `http://127.0.0.1:${destination.address().port}/result`;
  const source = http.createServer((_req, res) => {
    res.writeHead(200, {
      'Content-Type': 'text/html; charset=utf-8',
      'Referrer-Policy': 'strict-origin-when-cross-origin',
      'Cache-Control': 'no-store',
    });
    const tagged = campaignUrl(target, 'local_demo').replaceAll('&', '&');
    res.end(`<!doctype html><meta charset="utf-8">
      <h1>Referrer and UTM local demo</h1>
      <p><a id="normal" href="${target}">1. Normal link</a></p>
      <p><a id="noreferrer" rel="noreferrer" href="${target}">2. noreferrer</a></p>
      <p><a id="tagged" rel="noreferrer" href="${tagged}">3. noreferrer + UTM</a></p>`);
  });
  source.listen(0, '127.0.0.1', () => {
    console.log(`Open http://127.0.0.1:${source.address().port}/`);
  });
});

表示されたOpen ...のURLをブラウザで開き、各リンクをクリックします。1回確認するごとに送信元ページへ戻ってください。終了するときはターミナルでCtrl+Cを押します。

URLをアドレスバーに直接貼り付けるだけでは、このリンク遷移の比較にはなりません。3種類とも、送信元ページのアンカーから移動するのが確認のポイントです。

3. 実測では、参照元がなくてもUTMは残った

2026年10月19日に、上の方式で3種類のアンカーを実際にクリックしました。動的に割り当てられるポート番号を除く結果は次のとおりです。

クリックしたリンク 受信したReferer 受信したutm_source
通常のリンク 送信元のオリジン null
rel="noreferrer" null null
rel="noreferrer"+UTM null qiita

3番目ではutm_medium=referralとutm_campaign=20261019_link_observabilityも受信できました。

ここで確認できたのは、このローカル条件で、ブラウザが何を送信したかです。GA4での集計、同意管理との連携、広告ブロッカーの影響、購入への貢献は検証していません。ローカルHTTPの結果を、あらゆるブラウザや本番環境の保証として扱うこともできません。

4. 文字列連結より、URLとsearchParamsで付ける

既存URLに?lang=jaがあるのに、さらに?utm_source=...を足すと扱いにくくなります。上のcampaignUrl()はURLとsearchParams.set()を使って、既存のクエリとフラグメントを保ちつつ、対象のUTMを設定します。

次の確認コードを、campaignUrl()と一緒に実行できます。

import assert from 'node:assert/strict';

const url = new URL(campaignUrl('https://example.com/tool?lang=ja#demo', 'sample'));
assert.equal(url.searchParams.get('lang'), 'ja');
assert.equal(url.hash, '#demo');
assert.equal(url.searchParams.get('utm_source'), 'qiita');
assert.equal(url.searchParams.get('utm_content'), 'sample');
const twice = new URL(campaignUrl(url.href, 'sample'));
assert.equal(twice.searchParams.getAll('utm_source').length, 1);
assert.throws(() => campaignUrl('javascript:alert(1)', 'sample'), TypeError);

この6つのアサーションは、記事の準備時にNode.jsで実行して通過を確認しました。URL処理のテストであって、GA4の動作テストではありません。

実際の外部リンクでは、掲載元・記事・リンク位置に一貫した名前を付けます。このページでは、関係を開示したうえで、運営に関わるImyaとTryonrを、別々のutm_contentを付けた例にしています。リンク先の商品比較や推奨順位を示すものではありません。

個人のメールアドレス、ユーザーID、入力したプロンプト、決済情報はUTMに入れません。また、UTMは閲覧者が変更できる値です。認証やアクセス権限の判断に使うべきではありません。

5. GA4で確認するときに、飛ばしてはいけない境界

GoogleのカスタムURLによるキャンペーン計測の説明では、UTM付きURLを使い、トラフィック獲得レポートのセッションの参照元・メディアなどで確認する方法が案内されています。

ただし、URLにUTMが見えるだけでは集計成功の証拠にはなりません。次の順番で切り分けると、原因を混同しにくくなります。

  1. 掲載後の実際のhrefにUTMが残っているか確認する。
  2. リダイレクトやURL書き換えの後も、着地先で値が残るか確認する。
  3. 本番では同意管理の方針を守り、許可された計測イベントが送信されているか確認する。
  4. セッション単位とイベント単位の参照元を混ぜず、対応するディメンションで読む。
  5. 生成完了、結果表示、チェックアウト作成、支払い完了を、別の出来事として扱う。

同意を得られない場合、タグがブロックされた場合、既存セッションで遷移した場合などは、想定どおりの集計にならないことがあります。計測のために同意管理を迂回したり、参照元を隠す指定を無断で外したりする必要はありません。

購入については、ブラウザのボタンクリックを注文数の代わりにしないことも重要です。最終的には、重複排除された注文・決済の記録との照合が別途必要です。

よくある疑問

noreferrerを外せば解決しますか?

必ずしも解決しません。掲載元のプライバシー方針もあります。まずは許可されたリンクにUTMを付け、実際の着地と計測の有無を分けて確認します。

nofollowも参照元を隠しますか?

nofollowとnoreferrerは別の指定です。リンクを調べるときは、単にrelがあるかではなく、その値を確認します。検索向けのリンク属性とブラウザの参照元制御を混同しないようにします。

UTMが付いていれば、誰が買ったか分かりますか?

UTMだけでは分かりません。掲載元を区別するラベルであり、個人の特定や支払い確認の仕組みではありません。このサンプルも個人単位の追跡を実装していません。

まとめ

外部リンクの流入元が見えないときは、「参照元情報が届かない」「キャンペーンラベルが残らない」「計測されない」を分けて調べます。今回のローカル検証では、noreferrerによってRefererが省略されても、UTMは受信できました。

UTMは観測を補助するラベルです。クリック数、GA4のセッション数、支払い完了数を同じ数字として扱わないところから、計測の確認を始められます。

参考資料

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?