PageSourceSearch

https://fortune-cookie.tokyo/assets/js/fortune-cookie-namespace.js?v=1.0.610

js fortune-cookie.tokyo collected 2026-10-07 17:57:50 UTC 12,202 bytes, 252 lines download raw bytes

1/**
2 * Fortune Cookie グローバル名前空間
3 *
4 * グローバル変数の汚染を防ぐため、すべてのアプリケーション固有の
5 * データとメソッドをこの名前空間内に配置します。
6 */
7
8(function() {
9    'use strict';
10
11    // 名前空間の初期化(既に存在する場合は上書きしない)
12    window.FortuneCookie = window.FortuneCookie || {};
13
14    // GTM関連
15    window.FortuneCookie.gtm = {
16        debug: false,
17        eventHistory: [],
18
19        /**
20         * GTMイベントを発火
21         * @param {string} eventName - イベント名
22         * @param {Object} eventData - イベントデータ
23         */
24        fireEvent: function(eventName, eventData) {
25            // iframe 内 (例: /payment_method が purchase-popup-iframe 経由で読み込まれた場合) は
26            // GTM が親フレームにしか居ないので親へ postMessage で中継する。
27            // 親側の受信 handler は resources/views/components/purchase-popup.blade.php:191-194 が
28            // 'purchase-popup:ga4-events' を受け取り window.FortuneCookie.gtm.fireEvents(...) を呼ぶ。
29            if (window.parent !== window) {
30                try {
31                    // event_callback 等の関数参照は postMessage の structured clone を越えられないので
32                    // 事前に落としておく (呼び出し側の fireEventThenRun が iframe 分岐で対応)
33                    var dataForRelay = Object.assign({}, eventData || {});
34                    delete dataForRelay.event_callback;
35                    delete dataForRelay.event_timeout;
36                    // page_location / page_path / page_title を iframe 自身の URL で明示。
37                    // 親フレームへ中継する GA4 イベントは、明示しないと親の URL (TOP 等) が
38                    // 自動収集値として送信されてしまい、GA4 上で「/payment_method の button click が
39                    // TOP で発火した」ように見える (Notion ③ の見え方の原因)。event params の明示値は
40                    // GA4 の auto-collect より優先されるため、iframe の URL で上書きする。
41                    // 呼び出し側で既に page_location 等が指定されていればそちらを優先 (二重上書きしない)。
42                    if (dataForRelay.page_location === undefined) dataForRelay.page_location = window.location.href;
43                    if (dataForRelay.page_path === undefined) dataForRelay.page_path = window.location.pathname;
44                    if (dataForRelay.page_title === undefined) dataForRelay.page_title = document.title;
45                    window.parent.postMessage(
46                        { type: 'purchase-popup:ga4-events', events: [{ event: eventName, data: dataForRelay }] },
47                        window.location.origin
48                    );
49                } catch (e) {
50                    if (window.console) console.warn('[FortuneCookie.gtm.fireEvent] parent postMessage failed:', e);
51                }
52                return;
53            }
54
55            window.dataLayer = window.dataLayer || [];
56
57            // GA4 予約語衝突キー(source 等)を click_ プレフィックス付きへ付替。
58            // 呼び出し側は従来どおり { source: ... } を渡してよく、ここで吸収する。
59            // spec: docs/features/ga4-source-rename/spec.md
60            var normalizedEventData = this._renameConflictingKeys(eventData || {});
61
62            // dataLayer から ab_experiments を読み出し(GA4-3: 全イベントに exp_* を付与)
63            var abExperiments = {};
64            for (var i = window.dataLayer.length - 1; i >= 0; i--) {
65                if (window.dataLayer[i] && window.dataLayer[i].ab_experiments) {
66                    abExperiments = window.dataLayer[i].ab_experiments;
67                    break;
68                }
69            }
70
71            // ab_experiments の各キー(exp_xxx 形式)を eventData にマージ
72            // GA4-2: ab_experiments が空オブジェクト {}
72 の場合、exp_* キーは追加されない
73            const timestamp = new Date().toISOString();
74            const fullEventData = Object.assign(
75                { event: eventName },
76                abExperiments,
77                normalizedEventData
78            );
79
80            // dataLayerにpush
81            window.dataLayer.push(fullEventData);
82
83            // 発火履歴に記録(付替後の値を保存し、実際に GA4 へ送った内容と一致させる)
84            this.eventHistory.push({
85                timestamp: timestamp,
86                event: eventName,
87                data: normalizedEventData
88            });
89
90            // デバッグモード時はコンソール出力
91            if (this.debug) {
92                console.group('GA4 Event: ' + eventName);
93                console.log('Timestamp:', timestamp);
94                console.log('Original Event Data:', eventData);
95                console.log('Normalized Event Data:', normalizedEventData);
96                console.log('User ID:', window.userId || 'not set');
97                console.log('Full dataLayer Object:', fullEventData);
98                console.groupEnd();
99            }
100        },
101
102        /**
103         * GTMイベントをまとめて発火(サーバー側セッションキュー・postMessage 中継用)
104         * @param {Array<{event: string, data: Object}>} events
105         */
106        fireEvents: function(events) {
107            // iframe 内なら 1 回の postMessage でまとめて親へ中継する (関数参照は除去)
108            if (window.parent !== window) {
109                try {
110                    // fireEvent 側と同じ理由で page_location / page_path / page_title を iframe URL で明示する。
111                    var pageLocation = window.location.href;
112                    var pagePath = window.location.pathname;
113                    var pageTitle = document.title;
114                    var relayEvents = (events || []).map(function(e) {
115                        var data = Object.assign({}, e.data || {});
116                        delete data.event_callback;
117                        delete data.event_timeout;
118                        if (data.page_location === undefined) data.page_location = pageLocation;
119                        if (data.page_path === undefined) data.page_path = pagePath;
120                        if (data.page_title === undefined) data.page_title = pageTitle;
121                        return { event: e.event, data: data };
122                    });
123                    window.parent.postMessage(
124                        { type: 'purchase-popup:ga4-events', events: relayEvents },
125                        window.location.origin
126                    );
127                } catch (e) {
128                    if (window.console) console.warn('[FortuneCookie.gtm.fireEvents] parent postMessage failed:', e);
129                }
130                return;
131            }
132            var self = this;
133            (events || []).forEach(function(e) {
134                self.fireEvent(e.event, e.data);
135            });
136        },
137
138        /**
139         * GTM イベントを発火し、送信完了(GA4 の event_callback)を待って callbackFn を実行する。
140         * タイムアウト(1000ms)でも遷移をブロックしない。callback は先着 1 発ガードで
141         * 二重実行しない(event_callback とタイムアウトの race 対策)。
142         *
143         * spec: docs/features/add-payment-info-event/spec.md
144         *
145         * @param {string} eventName - イベント名
146         * @param {Object} eventData - イベントデータ
147         * @param {Function} callbackFn - 送信完了 / タイムアウトのいずれか先着で 1 回だけ実行される
148         */
149        fireEventThenRun: function(eventName, eventData, callbackFn) {
150            // iframe 内では fireEvent が親へ postMessage 中継するため event_callback (関数参照)
151            // が使えない (structured clone を越えられない)。postMessage 送信直後に callback を
152            // 実行する fire-and-forget 方式にする (親 → GTM 到達までのラグは飲む・
153            // 決済導線を 1.1s 待たせない)。
154            if (window.parent !== window) {
155                this.fireEvent(eventName, eventData);
156                try {
157                    callbackFn && callbackFn();
158                } catch (e) {
159                    if (window.console) console.warn('[FortuneCookie.gtm.fireEventThenRun] callback threw (iframe path):', e);
160                }
161                return;
162            }
163            var dispatched = false;
164            var run = function() {
165                if (dispatched) return;
166                dispatched = true;
167                try {
168                    callbackFn && callbackFn();
169                } catch (e) {
170                    if (window.console) console.warn('[FortuneCookie.gtm.fireEventThenRun] callback threw:', e);
171                }
172            };
173            // event_callback / event_timeout を eventData に載せる。
174            // GTM の GA4 タグでは gtag.js の event_callback / event_timeout がそのまま透過する。
175            var params = Object.assign({}, eventData || {}, {
176                event_callback: run,
177                event_timeout: 1000,
178            });
179            this.fireEvent(eventName, params);
180            // event_callback が非対応 / GTM 側で発火しない環境向けの保険。
181            // 1000ms + 少しの余裕を持ってタイムアウト。dispatched フラグで二重実行を防ぐ。
182            setTimeout(run, 1100);
183        },
184
185        enableDebug: function() {
186            this.debug = true;
187            console.log('GA4 Debug mode enabled');
188        },
189
190        disableDebug: function() {
191            this.debug = false;
192            console.log('GA4 Debug mode disabled');
193        },
194
195        showHistory: function(limit) {
196            var history = limit ? this.eventHistory.slice(-limit) : this.eventHistory;
197
198            if (history.length === 0) {
199                console.log('No events fired yet');
200                return;
201            }
202
203            console.table(history.map(function(h) {
204                return {
205                    Timestamp: h.timestamp,
206                    Event: h.event,
207                    Data: JSON.stringify(h.data)
208                };
209            }));
210        },
211
212        clearHistory: function() {
213            this.eventHistory = [];
214            console.log('Event history cleared');
215        },
216
217        /**
218         * GA4 予約語衝突キー(source / medium / campaign / term / content)を
219         * click_ プレフィックス付きへ付替する。
220         *
221         * - トップレベルのキーのみ対象(items[] などネスト構造は触らない)
222         * - 新キーが既にある場合は新キー優先で旧キーを捨てる
223         * - 冪等(新キーだけの入力に対しては no-op)
224         *
225         * spec: docs/features/ga4-source-rename/spec.md
226         *
227         * @param {Object} data
228         * @returns {Object} 付替後の新しいオブジェクト(入力は変更しない)
229         */
230        _renameConflictingKeys: function(data) {
231            var renameMap = {
232                source: 'click_source',
233                medium: 'click_medium',
234                campaign: 'click_campaign',
235                term: 'click_term',
236                content: 'click_content'
237            };
238            var out = Object.assign({}, data || {});
239            for (var oldKey in renameMap) {
240                if (!Object.prototype.hasOwnProperty.call(renameMap, oldKey)) continue;
241                if (!Object.prototype.hasOwnProperty.call(out, oldKey)) continue;
242                var newKey = renameMap[oldKey];
243                if (!Object.prototype.hasOwnProperty.call(out, newKey)) {
244                    out[newKey] = out[oldKey];
245                }
246                delete out[oldKey];
247            }
248            return out;
249        }
250    };
251
252})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.