diff --git a/app/client.ts b/app/client.ts index 2a5ee69e..501841e5 100644 --- a/app/client.ts +++ b/app/client.ts @@ -22,6 +22,32 @@ const gtagFn: GtagFn = import.meta.env.PROD console.log('[gtag]', command, name, params) } +interface Twq { + (...args: unknown[]): void + queue?: unknown[][] +} + +const xPixelFn = import.meta.env.PROD + ? () => { + const w = window as Window & { twq?: Twq } + if (typeof w.twq === 'function') { + w.twq('event', 'tw-ov0j6-ov0j9', {}) + } else { + // Initialize stub + queue if the base X pixel script (uwt.js) has not loaded yet. + // Mirrors the queuing behavior of the X base IIFE so early calls are not lost. + const queue: unknown[][] = [] + const stub: Twq = (...args: unknown[]) => { + queue.push(args) + } + stub.queue = queue + w.twq = stub + stub('event', 'tw-ov0j6-ov0j9', {}) + } + } + : () => { + console.log('[x-pixel]', 'event', 'tw-ov0j6-ov0j9', {}) + } + setupScrollDepthTracking({ gtagFn, addScrollListener: (handler) => window.addEventListener('scroll', handler, { passive: true }), @@ -83,4 +109,4 @@ setupMakamujoBannerTracking({ maxDelayMs: 500, }) -setupDownloadAdPopup({ whenReady, gtagFn }) +setupDownloadAdPopup({ whenReady, gtagFn, xPixelFn }) diff --git a/app/components/DownloadAdDialog.test.ts b/app/components/DownloadAdDialog.test.ts index f6cc2190..e266fa2d 100644 --- a/app/components/DownloadAdDialog.test.ts +++ b/app/components/DownloadAdDialog.test.ts @@ -97,6 +97,7 @@ function makeSetup( startDownload?: ReturnType hasDownloadUi?: () => boolean gtagFn?: ReturnType + xPixelFn?: ReturnType getPagePath?: () => string baseUrl?: string } = {}, @@ -119,6 +120,7 @@ function makeSetup( }, startDownload: startDownloadFn, gtagFn: opts.gtagFn, + xPixelFn: opts.xPixelFn, getPagePath: opts.getPagePath, baseUrl: opts.baseUrl, }) @@ -277,6 +279,39 @@ describe('setupDownloadAdPopup', () => { expect(gtagFn).not.toHaveBeenCalled() }) + it('fires X pixel conversion when xPixelFn is provided', () => { + const button = { + ...makeFakeButton('./test.pdf'), + textContent: 'ダウンロード', + } + const xPixelFn = vi.fn() + const { triggerClick } = makeSetup(button, { xPixelFn }) + + triggerClick() + + expect(xPixelFn).toHaveBeenCalledTimes(1) + }) + + it('fires both GA and X pixel on download when both fns provided', () => { + const button = makeFakeButton() + const gtagFn = vi.fn() + const xPixelFn = vi.fn() + const { triggerClick } = makeSetup(button, { gtagFn, xPixelFn }) + + triggerClick() + expect(gtagFn).toHaveBeenCalled() + expect(xPixelFn).toHaveBeenCalled() + }) + + it('does not fire X pixel when xPixelFn is omitted', () => { + const button = makeFakeButton() + const xPixelFn = vi.fn() + const { triggerClick } = makeSetup(button) + + triggerClick() + expect(xPixelFn).not.toHaveBeenCalled() + }) + it('opens cross-origin downloads in a new tab', () => { const button = makeFakeButton('https://example.com/file.zip', { sameOrigin: false, newTab: true }) const { startDownloadFn, triggerClick } = makeSetup(button) diff --git a/app/components/DownloadAdDialog.tsx b/app/components/DownloadAdDialog.tsx index 0690065a..130ad8bd 100644 --- a/app/components/DownloadAdDialog.tsx +++ b/app/components/DownloadAdDialog.tsx @@ -36,6 +36,8 @@ export interface DownloadAdPopupOptions { baseUrl?: string /** Sends GA4 `file_download` events when a download button is clicked. */ gtagFn?: GtagFn + /** Sends X Ads conversion event when a download button is clicked. */ + xPixelFn?: () => void /** @internal Test override for the current page path sent as `link_id`. */ getPagePath?: () => string } @@ -128,6 +130,7 @@ export function setupDownloadAdPopup({ hasDownloadUi = defaultHasDownloadUi, baseUrl, gtagFn, + xPixelFn, getPagePath = () => (typeof window !== 'undefined' ? window.location.pathname : ''), }: DownloadAdPopupOptions): void { whenReady(() => { @@ -150,6 +153,9 @@ export function setupDownloadAdPopup({ const linkText = button.textContent?.trim() ?? '' trackFileDownload(gtagFn, href, linkText, getPagePath()) } + if (xPixelFn) { + xPixelFn() + } startDownloadFn(href, download, newTab) }) }) diff --git a/app/components/RootLayout.tsx b/app/components/RootLayout.tsx index 31f1d22f..7dd038ea 100644 --- a/app/components/RootLayout.tsx +++ b/app/components/RootLayout.tsx @@ -153,6 +153,19 @@ const gtagSnippets = { `, } as const +const xPixelSnippets = { + body: html`\ + + + +`, +} as const + const OpenGraph = ({ url, image }: OpenGraphData) => url || image ? ( <> @@ -203,6 +216,7 @@ const Layout = (props: PropsWithChildren) => html` ${props.children} ${import.meta.env.PROD ? gtagSnippets.body : html`\n`}\ + ${import.meta.env.PROD && props.downloadAdPopup ? xPixelSnippets.body : html`\n`}\ `