月初に前月の数字を並べていたときのことです。AdMob の管理画面が示す一日と、App Store Connect の売上レポートが指す一日と、手元のスクリプトが書き出したログの一日が、そろって少しずつずれておりました。
月の合計はほぼ合うのに、日別に並べると端の二日だけが合いません。最初は抽出の取りこぼしを疑って、条件を何度も書き換えました。原因は抽出ではなく、三つのデータがそれぞれ別の「一日の切れ目」を持っていたことでした。
Claude Code の v2.1.257 で Time format と timeZone の設定が入り、タイムスタンプの見え方を手元で決められるようになりました。選択肢が増えたのはありがたいのですが、同時に迷いも増えます——表示を寄せて読みやすくするのか、記録そのものを固定してしまうのか。
私はしばらく前者から入って、あとで戻ってきました。順番を間違えていたのだと気づいたのは、二度目の月初が来たあとでした。その往復で分かったことを書き残します。
表示で寄せる道と、記録で固定する道
二つは目的が違います。混ぜて考えると、どちらを触っているのか自分でも分からなくなります。
観点 表示で寄せる 記録で固定する
触る場所 CLI やビューアの設定 ログを書き出すコード
効く範囲 これから自分が読む画面だけ これから書かれる全ての行
過去のデータ 解釈が変わるだけで中身は同じ 過去分は変わらないので移行が要る
他人・他ツールへの影響 なし 読み手すべてに及ぶ
取り消しやすさ 設定を戻すだけ 書式が混ざるので戻しにくい
表示側は、Claude Code の設定ファイルに一行足すだけで済みます。
{
"timeZone" : "Asia/Tokyo"
}
記録側は、書き出す関数を通す形になります。私の場合は集計スクリプトの入口に一つ関数を置きました。
from datetime import datetime, timezone, timedelta
JST = timezone(timedelta( hours = 9 ))
def to_fixed_offset (value, assume = None ):
"""ログに書く直前に必ず通す。オフセットの無い値はここで落とします。"""
if isinstance (value, str ):
# "2026-09-01T03:12:00Z" のような Z 終わりも受けます
value = datetime.fromisoformat(value.replace( "Z" , "+00:00" ))
if value.tzinfo is None :
if assume is None :
raise ValueError (
f "オフセットの無い時刻を受け取りました: { value !r } "
"(取得元のタイムゾーンを assume= で明示してください)"
)
value = value.replace( tzinfo = assume)
# 記録は UTC 固定。読むときに寄せる前提なので、ここでは寄せません
return value.astimezone(timezone.utc).isoformat( timespec = "seconds" )
assume を必須にせず、既定値も置かなかったのが要点です。オフセットの無い時刻が来たときに黙って補完してしまうと、どのデータがどの前提で入ってきたのかが二度と分からなくなります。落ちてくれたほうが安全なのです。
表示側に寄せたときに、私が見落としたもの
最初のうち、私は表示側だけで済ませようとしすぎました。設定を一行足せば画面の時刻が全部 JST になりますから、読むのは確かに楽になります。
結果は芳しくありませんでした。表示が揃うと、ずれていることに気づけなくなるのです。
AdMob のレポートは太平洋時間の一日で締まり、App Store Connect の売上レポートはまた別の基準を持ちます。手元のログはコンテナの中で書かれていて UTC でした。それらを画面上で全部 JST に見せてしまうと、三つとも同じ物差しで測られたように見えます。実際には測り方が違うまま並んでいるだけでした。
端の二日だけが合わなかったのは、そのためです。本番運用のデータで起きるこの種の落とし穴は、エラーとして表に出てこないぶん厄介なのかもしれません。真ん中の日はどの基準で切っても大差が出ないので、月の合計を見ているかぎり何も起きていないように見えます。おかしいと気づけるのは月初と月末だけで、そこは同時に「まだ確定していない日」でもあるため、確定待ちだと思って見送っていました。
表示を揃えることは、読みやすさを買う代わりに、差分の手がかりを一つ手放すことでした。 この線引きに気づくまでに、私は同じ集計を三度書き直しております。
記録側を固定するときに決めた三つのこと
記録側に手を入れる方針に切り替えたあと、決めごとを三つだけ置きました。多くすると守れなくなります。
記録は UTC で、必ずオフセット付きで書きます。 2026-09-01T03:12:00+00:00 のように、あとから読む人が推測しなくてよい形にします。末尾の Z も受け入れますが、書くときは明示的なオフセットに揃えました。
オフセットの無い値は受け取りません。 上の関数のように例外で落とします。取得元が naive な時刻しか返さない場合は、その取得元ごとに assume を書いて、どこで前提を置いたのかをコードに残します。
「一日」の定義は、データごとに別の名前で持ちます。 手元の集計では report_day_admob と report_day_appstore と run_day_jst を別の列として持ち、同じ date という名前を三つのものに使うのをやめました。
三つ目がいちばん効きました。名前が同じだと、あとから読んだ自分が無意識に同じものとして扱ってしまいます。列名を分けた時点で、突き合わせのコードに「どちらの一日で並べるのか」を書かざるを得なくなります。
Stripe の決済ログを同じ表に混ぜたときも、この作法がそのまま使えました。決済側は UTC で揃っているので、寄せるのではなく、寄せ先を明示した列をもう一本足すだけで済んでおります。
混在を見つける検査を、突き合わせの前に置く
移行の途中がいちばん危ない時期でした。新しく書かれる行はオフセット付きで、古い行は naive のまま残ります。この状態で集計を走らせると、片方だけが九時間ずれた結果が出ます。
そこで、突き合わせを始める前に走らせる検査を書きました。
import re
import sys
from collections import Counter
from pathlib import Path
TS = re.compile( r " \b(\d {4} - \d {2} - \d {2} )[ T ](\d {2} : \d {2} : \d {2} )( Z | [ +- ]\d {2} : ? \d {2} ) ? " )
def scan (log_dir):
kinds = Counter()
samples = {}
for path in Path(log_dir).rglob( "*.log" ):
for lineno, line in enumerate (path.read_text( encoding = "utf-8" , errors = "replace" ).splitlines(), 1 ):
m = TS .search(line)
if not m:
continue
offset = m.group( 3 )
kind = "naive" if offset is None else ( "utc" if offset in ( "Z" , "+00:00" , "+0000" ) else offset)
kinds[kind] += 1
samples.setdefault(kind, f " { path.name } : { lineno } : { line[: 80 ] } " )
return kinds, samples
if __name__ == "__main__" :
kinds, samples = scan(sys.argv[ 1 ])
for kind, count in kinds.most_common():
print ( f " { kind :>10 } { count :>7 } { samples[kind] } " )
# 種類が 1 つに収束するまでは、集計を走らせない
if len (kinds) > 1 :
print ( " \n 混在しています。集計の前に書式を揃えてください。" )
sys.exit( 1 )
最後の二行が本体です。移行期の事故を回避したいのであれば、判断を人に残さないほうが確実だと感じています。検査結果を人が読んで判断する形にしておくと、忙しい朝には読み飛ばします。終了コードで止めてしまえば、判断そのものが要らなくなります。
この検査を入れてから、私は移行が終わる前に集計を走らせてしまう事故を起こさなくなりました。止まった回数そのものは多くありませんが、止まったときは毎回、本当に混ざっておりました。
二つを組み合わせたあとの形
いまは両方を使っています。記録は UTC のオフセット付きで固定し、表示だけを JST に寄せる形です。
層 決めたこと 理由
書き出し UTC・オフセット必須 あとから読む人が推測しなくてよい
保存・突き合わせ 変換しない 変換した瞬間に元の基準が消える
表示 JST へ寄せる 読むのは自分で、頭の中の時計は JST
調査の入口 寄せる前の生の値を一度見る ずれの手がかりを残しておく
四行目だけは仕組みではなく習慣です。数字が合わないときに最初に開くのは、整形済みの画面ではなく生のログにしています。整形は疑問を持ったあとの読みやすさのためにあるもので、疑問を持つきっかけを作ってはくれません。
Claude Code の timeZone は、まさにこの四行目より上の層に効く設定です。表示を寄せる手段が公式に用意されたぶん、記録側を素のままにしておく言い訳が一つ減ったと受け取っております。
どちらから手をつけるかの判断基準
環境の形によって、先に触るべき側が変わります。私が使っている目安を並べます。
データの出どころが一つだけなら、表示側から。 手元のスクリプトしか書いていないログを自分だけが読むのであれば、記録の書式を変える手間に見合いません。この場合は設定の一行で十分です。
出どころが三つ以上あるなら、記録側から。 私の場合は広告・ストア・決済の三系統があり、この時点で表示だけでは足りませんでした。
他の人やツールがそのログを読むなら、記録側から。 表示設定は読み手ごとに違うので、揃うことを期待できません。
過去分を捨てられないなら、混在検査を先に。 書式を変える判断より、混ざっている期間を可視化するほうが先に来ます。移行を採用する場合は、検査を入れてから書式を変える順序を推奨します。
寄せる判断は読み手のためのもので、固定する判断は書き手の責任のためのものです。 順番を間違えると、読みやすい画面の裏で前提が混ざり続けます。私はこの順番だけは、急いでいる日でも崩さないようにしております。
明日の朝、最初に確かめていただきたいこと
集計や監視のログを一本だけ開いて、タイムスタンプの末尾にオフセットが付いているかどうかを見てください。付いていなければ、そのログはいま何の前提で書かれているのかを、書いた本人しか知らない状態にあります。
そこから始めていただければ、上の検査スクリプトを入れるかどうかは自然に決まってくるはずです。私も、月末の二日が合わない理由を探しているときに、まさにその一行を眺めるところからやり直しました。
お読みいただきありがとうございました。