From 7c2bf2cdaab0443ab0ef1cd5a5353656507582b6 Mon Sep 17 00:00:00 2001 From: appdevelopsk Date: Thu, 17 Sep 2026 22:00:05 +0900 Subject: [PATCH] =?UTF-8?q?feat(analytics):=20=E5=85=B1=E6=9C=89=E5=B0=8E?= =?UTF-8?q?=E7=B7=9A=E3=81=AB=20share=20=E3=82=A4=E3=83=99=E3=83=B3?= =?UTF-8?q?=E3=83=88=E3=82=92=E8=A8=88=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ShareBar の共有導線が一切計測されていなかった。events.ts の export は trackCalculate / trackCopyResult / trackPresetClick の3本のみで share は 関数すら存在せず、nativeShare() と SNS 5件(X / Facebook / LINE / WhatsApp / Reddit)は無計装。クリップボードの copy_result だけが計測され ていたため、共有施策の効果判定が原理的に不可能だった。 - events.ts: trackShare を既存の emit/toParams 形に合わせて追加 - useToolEvents.ts: share を返す。calculate と違い間引かない (共有先ごとの内訳が目的で、複数先への共有は正当な行動のため) - ShareBar.tsx: navigator.share 成功時と各 SNS リンクの onClick で送出 nativeShare() のフォールバックは copy() が copy_result("link") を送るため share は送らない(1操作を2イベントで数えると共有率が二重計上になる)。 ShareBar は ToolFrame からのみ使われるため全ツールへ一括で効く。 GA4 Admin > Custom definitions への label 登録が別途必要 (未登録だと Data API 側が空になる)。 npm run typecheck / audit:i18n ともに PASS。 Claude-Session: https://claude.ai/code/session_01ENHPNZ5ngcdrTHJ2H6oAFd --- site/src/components/tools/ShareBar.tsx | 8 +++++++- site/src/lib/analytics/events.ts | 12 ++++++++++++ site/src/lib/analytics/useToolEvents.ts | 11 +++++++++-- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/site/src/components/tools/ShareBar.tsx b/site/src/components/tools/ShareBar.tsx index 1a151320..837e186c 100644 --- a/site/src/components/tools/ShareBar.tsx +++ b/site/src/components/tools/ShareBar.tsx @@ -85,7 +85,7 @@ const btn = export function ShareBar({ url, embedUrl, title, slug }: Props) { const t = useTranslations("tool"); - const { copyResult } = useToolEvents(slug); + const { copyResult, share } = useToolEvents(slug); const [linkCopied, setLinkCopied] = useState(false); const [resultCopied, setResultCopied] = useState(false); const [hasResultLink, setHasResultLink] = useState(false); @@ -140,11 +140,15 @@ export function ShareBar({ url, embedUrl, title, slug }: Props) { if (typeof navigator !== "undefined" && navigator.share) { try { await navigator.share({ title, url }); + // 成功時だけ送る。キャンセルは reject されるのでここには来ない。 + share("native"); return; } catch { /* user cancelled — fall through */ } } + // フォールバックは copy() 側が copy_result("link") を送るため share は送らない + // (同一操作を2イベントで数えると共有率が二重計上になる)。 copy(url, setLinkCopied, "link"); } @@ -179,6 +183,8 @@ export function ShareBar({ url, embedUrl, title, slug }: Props) { className={btn} aria-label={`${t("share")}: ${name}`} title={name} + // target="_blank" なので現ページは破棄されず、遷移前の同期送出で取りこぼさない。 + onClick={() => share(name)} > diff --git a/site/src/lib/analytics/events.ts b/site/src/lib/analytics/events.ts index 2e10e060..afee2cc0 100644 --- a/site/src/lib/analytics/events.ts +++ b/site/src/lib/analytics/events.ts @@ -94,3 +94,15 @@ export function trackCopyResult(p: ToolEventParams): void { export function trackPresetClick(p: ToolEventParams): void { emit("preset_click", toParams(p)); } + +/** + * 共有導線を踏んだとき(ネイティブ共有・SNSリンク)。 + * + * copy_result はクリップボードのみを見ており、ShareBar の主要導線 + * (navigator.share / X / Facebook / LINE / WhatsApp / Reddit)は + * 一切計測されていなかった。label に共有先を入れて内訳を取る。 + * 外部遷移は計測を待たずに起きるので、送出は必ず遷移前に行う。 + */ +export function trackShare(p: ToolEventParams): void { + emit("share", toParams(p)); +} diff --git a/site/src/lib/analytics/useToolEvents.ts b/site/src/lib/analytics/useToolEvents.ts index 27dab39e..d851ee00 100644 --- a/site/src/lib/analytics/useToolEvents.ts +++ b/site/src/lib/analytics/useToolEvents.ts @@ -1,7 +1,7 @@ "use client"; import { useCallback, useRef } from "react"; -import { trackCalculate, trackCopyResult, trackPresetClick } from "./events"; +import { trackCalculate, trackCopyResult, trackPresetClick, trackShare } from "./events"; /** * ツールコンポーネントから GA4 イベントを送るためのフック。 @@ -33,5 +33,12 @@ export function useToolEvents(slug: string) { [slug], ); - return { calculate, copyResult, presetClick }; + // share は calculate と違い間引かない。共有先(X / LINE / native ...)ごとの + // 内訳を取るのが目的で、同一着地で複数先に共有するのは正当な行動のため。 + const share = useCallback( + (label?: string) => trackShare({ tool: slug, ...(label ? { label } : {}) }), + [slug], + ); + + return { calculate, copyResult, presetClick, share }; }