決済レポートは150.00。分析データのエクスポートも150.00。どちらにも異なる注文IDが3つあります。これだけなら、連携は正常だと判断したくなります。
しかし、この確認を通過しても、両者が表している注文は違うかもしれません。
そこで、小さな反例を実際に動かしてみます。使うのは架空のデータです。顧客の障害事例でも、特定の分析サービスについての主張でもありません。合計の比較では何が確かめられないのか、そして次に何を調べるべきかを示します。
合計が等しくても、レコードが同じとは限らない
ある店舗には、支払い済みの注文が3件あります。O101が40.00、O102が60.00、O103が50.00です。一方、生の購入イベントのエクスポートには、O101が2回、O102が1回、対応する注文のないO999が10.00で記録されています。金額はすべて、小数点以下2桁を使う同じ架空の通貨です。
| 注文ID | 支払い済み注文の金額 | 生イベントの金額 | イベント行数 |
|---|---|---|---|
| O101 | 40.00 | 40.00 + 40.00 | 2 |
| O102 | 60.00 | 60.00 | 1 |
| O103 | 50.00 | イベントなし | 0 |
| O999 | この例に注文なし | 10.00 | 1 |
| 合計 | 150.00 | 150.00 | 4 |
重複で40.00が増え、対応する注文のないレコードでさらに10.00が増えます。この2つが、欠けた50.00をちょうど相殺します。異なる注文IDの数も双方とも3つですが、共通するIDは2つだけです。
この例ではイベント行数を比べると違いが分かります。合計と異なるIDの件数では分かりません。これらの集計値だけでは、どの注文が欠けたかも特定できません。

まず、2つのエクスポートの意味をそろえる
ブラウザーの購入イベントは、計測コードが報告した内容を記録します。支払い済み注文のレコードは、注文システムや決済システムが管理する業務上の状態を表します。答える問いが違うので、一方を説明なく他方の代わりにしてはいけません。
比較する前に、店舗またはアカウント、通貨、共通の注文識別子、含める決済状態、金額の定義、期間を決めます。注文単位、決済試行単位、明細単位のどれで比べるのかも明確にします。1つの注文に複数の決済や明細があること自体は正当です。
注文総額、売上確定した決済、返金後の純売上、手数料控除後の入金額は別の数値です。イベント時刻、決済時刻、取り込み時刻、入金時刻も同じではありません。締め切りをそろえ、実際の処理遅延を考慮してください。まだ到着していない新しいレコードは、必ずしも失われたわけではありません。
この例の条件は意図的に限定しています。完全な架空のスナップショット、支払い済み注文3件、注文ごとに期待する購入イベント1件です。返金、税金、手数料、分割決済、通貨換算はありません。これは生イベントの例であり、特定サービスの重複排除ルールの再現ではありません。
結合する前にイベント側を集約する
Python 3と、そのsqlite3モジュールで実行できます。メモリー内にデータベースを作るだけで、ネットワーク通信も本番レコードの変更も行いません。金額は架空の通貨の最小単位を整数で保持するため、比較は二進浮動小数点演算に依存しません。
import sqlite3
db = sqlite3.connect(":memory:")
db.executescript("""
CREATE TABLE orders (order_id TEXT PRIMARY KEY, cents INTEGER NOT NULL);
CREATE TABLE events (event_id TEXT PRIMARY KEY, order_id TEXT NOT NULL,
cents INTEGER NOT NULL);
INSERT INTO orders VALUES ('O101', 4000), ('O102', 6000), ('O103', 5000);
INSERT INTO events VALUES ('E1', 'O101', 4000), ('E2', 'O101', 4000),
('E3', 'O102', 6000), ('E4', 'O999', 1000);
""")
for table in ("orders", "events"):
total, ids = db.execute(
f"SELECT SUM(cents), COUNT(DISTINCT order_id) FROM {table}"
).fetchone()
print(f"{table}: cents={total}, distinct_order_ids={ids}")
query = """
WITH observed AS (
SELECT order_id, COUNT(*) AS event_rows, SUM(cents) AS event_cents
FROM events GROUP BY order_id
), ids AS (
SELECT order_id FROM orders UNION SELECT order_id FROM events
)
SELECT ids.order_id, o.cents, COALESCE(e.event_rows, 0), e.event_cents
FROM ids
LEFT JOIN orders o ON o.order_id = ids.order_id
LEFT JOIN observed e ON e.order_id = ids.order_id
ORDER BY ids.order_id
"""
for row in db.execute(query):
print(row)
db.close()
出力は次のとおりです。
orders: cents=15000, distinct_order_ids=3
events: cents=15000, distinct_order_ids=3
('O101', 4000, 2, 8000)
('O102', 6000, 1, 6000)
('O103', 5000, 0, None)
('O999', None, 1, 1000)
このクエリは、まずイベントを注文IDごとに1行へ集約します。注文1行を複数のイベント行に直接結合すると、注文金額が繰り返され、照合処理そのものが誤解を招くことがあります。また、両側のIDを合わせることで、対応先のないレコードも残します。内部結合では、それらが見えなくなります。
O103にはイベント行がありません。O999には、この例の中で対応する注文がありません。O101にはイベント行が2つあります。これは、理由の分からない1つの合計差ではなく、3つの異なる観察結果です。

実際のエクスポートでは、集約前に識別子の欠落も調べてください。欠落したIDを一律にunknownなどへ置き換えると、無関係なレコードが同じ注文に属するように見えてしまいます。IDの範囲は適切な店舗、アカウント、環境に限定します。双方でIDが違うなら、金額の一致や近いタイムスタンプで結合せず、対応関係を用意します。
不一致は調査の出発点であり、原因の確定ではない
この例では、問題を自分で作ったので原因が分かっています。本番では、対応先のないIDが、日付の区切りの違い、テスト環境、不完全なエクスポート、片方のレポートだけが除外する正当な業務状態から生じることもあります。レコードを調べるまでは、その可能性を残します。
ブラウザーからの送信と決済確認では、失敗の起こり方も異なります。StripeのCheckoutフルフィルメントガイドは、決済が成功しても戻り先ページへの訪問は保証されないと説明しています。そのページのイベントだけでは、完全な決済確認の仕組みになりません。
サーバー側の配信にも注意が必要です。StripeはWebhookの重複配信を説明しています。通知を受け取ることと、その業務上の効果を安全に1回だけ適用することは別です。プロバイダーのイベントIDと注文IDは、異なる識別の問題を扱います。別々のイベントが同じ注文に関係することもあります。この記事では決済ハンドラーを実装せず、万能な重複排除キーも提案しません。
同様に、sendBeacon()の戻り値がtrueでも、ブラウザーが送信待ちのキューにデータを追加したという意味にとどまります。分析アプリケーションが購入を受理して保存した証明ではありません。これは文書化された戻り値の意味であり、特定の収集システムがイベントを失った証拠ではありません。

アトリビューションの解釈より先に、注文を照合する
有用な照合では、期待するイベントのない支払い済み注文、対応する支払い済み注文のないイベント、繰り返されたイベント行、金額や通貨の不一致を別々のリストとして保持します。安定した比較条件で、これらを継続的に確認します。アトリビューションの図を合わせるためにチャネルを作り上げず、対応しない売上はそのまま見えるようにします。
計測の網羅性を改善しても、あるチャネルが購入を引き起こしたと証明できるわけではありません。アトリビューションは、成果をどう配分するかという別の問いです。そもそも購入レコードが一致するかという問いとは分けて考えます。
すべてのダッシュボードが間違っているという話ではありません。合計の一致は、見た目ほど強い証拠ではないということです。集計値の確認は残しつつ、売上連携の照合が完了したと判断する前に、レコード単位の比較も行ってください。



