0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

日別のアクセス記録に「数ではない鍵」を1つ足した記録を1つ前の版の集計に通すと、閲覧数は 62[object Object]1111 という文字列になった(例外は出ない)

0
Posted at

自分のサーバのアクセス記録(日ごとの JSON)を足し合わせて、ダッシュボードに「閲覧数」を出しています。2026-08-30 に、その日ごとの記録へ _tools という鍵を1つ足しました。値は数ではなく、ファイル名ごとの件数を持つオブジェクトです。

翌日、足す前の集計関数(1つ前のコミットのもの)に、足したあとの記録を通してみると、集計はこう返りました。

集計関数の版 閲覧数(pageViews) 型 合計(total)
鍵を足す前の版(除外は _from だけ) 62[object Object]1111 string 62[object Object]111131
鍵を足した版(除外に _tools を足した) 66 number 97
翌日直した版(_ で始まる鍵は数えない) 66 number 97

**例外は出ません。**数字の場所に、この文字列がそのまま入ります。

ただし、この文字列が実際の画面に出た経路は見つかっていません。_tools は、除外の一覧に _tools を足したのと同じコミットで初めて記録に入りました。その時点で記録を読んでいたコードは、私が読んだかぎり、どれもこの集計関数を通すか、読む鍵を名前で絞っていました。ただし画面の表示そのものは保存していないので、出なかったことを表示で確かめたわけではありません。

上の1行目は、「一覧を伸ばし忘れていたら」を、実際の記録と実際の1つ前の関数で確かめた数です。その日に一覧を伸ばしたのは、鍵を足すときに「足し込むと数がオブジェクトと混ざって壊れる」と気づいて、除外の行の上にそうコメントを書いたからです。止めたのは人が覚えていたことで、仕組みが止めたわけではありません。

1. 何が起きていたか

1つ前の版の集計は、こういう形でした(実ファイルから、関係する行だけを抜き出しています)。

for (const [k, n] of Object.entries(log[d] || {})) {
  if (k === "_from") continue;
  if (CRAWLER_FILES.has(k)) crawlerFetches += n; else pageViews += n;
}

日ごとの記録には、ページ名と件数のほかに、流入元の内訳を持つ _from という鍵が入っています。これはページではないので、名前で1つだけ除いていました。

そこへ _tools(値はオブジェクト)が入ると、pageViews += n の右辺がオブジェクトになります。JavaScript の + は、片方が数でもう片方が valueOf も toString も自分で持たない普通のオブジェクト({ ... })なら、オブジェクトを文字列 "[object Object]" にして文字列としてつなぎます(配列や valueOf を持つオブジェクトでは結果が変わります)。そこから先は pageViews が文字列なので、後ろの日の件数も数として足されず、件数の数字がそのまま後ろにつながっていきます(今回はどの件数も 1 だったので、1文字ずつに見えます)。62 は _tools より前に足し終えていた分、末尾の 1111 はその後ろに足した4件です。合計の total は、それにクローラ向けの件数 31 を「つないだ」ものになりました。

固定した数(動かないコミット 47c82bf^ / 47c82bf / 5c330d3 の集計関数と、5c330d3 の記録から出したもの):

  • 記録は 24日分(2026-08-05〜2026-08-31)
  • _ で始まる鍵は2種類。_from が22日、_tools が1日。値はどちらもオブジェクト
  • 鍵を足したコミットは 2026-08-30 19:24、集計を規則に直したコミットは 2026-08-31 19:20

2. 再現スクリプト

依存はありません。ファイルを読み書きせず、通信もしません。小さな作り物の記録で、3通りの集計を並べます。

// 日別の記録に「数ではない鍵」を1つ足したとき、集計の書き方ごとに何が返るか。
// 依存0・ファイルを読み書きしない・通信しない。
const log = {
  "2026-08-29": { "a.html": 3, "_from": { "直接": 3 } },
  "2026-08-30": { "a.html": 2, "_from": { "直接": 2 }, "_tools": { "b.html": 1 }, "b.html": 1 },
};

// 1. 除外を名前で列挙する(_from だけを知っている)
function denylist(log) {
  let sum = 0;
  for (const day of Object.values(log))
    for (const [k, n] of Object.entries(day)) {
      if (k === "_from") continue;
      sum += n;
    }
  return sum;
}

// 2. 1 に `|| 0` を足す
function orZero(log) {
  let sum = 0;
  for (const day of Object.values(log))
    for (const [k, n] of Object.entries(day)) {
      if (k === "_from") continue;
      sum += n || 0;
    }
  return sum;
}

// 3. 規則で外し(_ で始まる鍵はページではない)、数でないものは名指しして残す
function rule(log) {
  let sum = 0;
  const unreadable = [];
  for (const [d, day] of Object.entries(log))
    for (const [k, n] of Object.entries(day)) {
      if (k.startsWith("_")) continue;
      if (!Number.isFinite(n)) { unreadable.push(`${d} ${k}`); continue; }
      sum += n;
    }
  return { sum, unreadable };
}

// 3 に、数でない記録を1件混ぜたもの
const broken = structuredClone(log);
broken["2026-08-30"]["c.html"] = { oops: 1 };

const show = (v) => ({ value: v, type: typeof v });
const out = {
  node: process.version,
  denylist: show(denylist(log)),
  orZero: show(orZero(log)),
  rule: rule(log),
  ruleWithBroken: rule(broken),
  orZeroOfObject: show({ b: 1 } || 0),
  numberOfObject: String(Number({ b: 1 })), // NaN は JSON で null になるので文字で持つ
  isFiniteOfObject: Number.isFinite({ b: 1 }),
};
if (process.argv.includes("--json")) console.log(JSON.stringify(out, null, 2));
else for (const [k, v] of Object.entries(out)) console.log(k.padEnd(16), JSON.stringify(v));

出力(Node v24.18.1):

node             "v24.18.1"
denylist         {"value":"5[object Object]1","type":"string"}
orZero           {"value":"5[object Object]1","type":"string"}
rule             {"sum":6,"unreadable":[]}
ruleWithBroken   {"sum":6,"unreadable":["2026-08-30 c.html"]}
orZeroOfObject   {"value":{"b":1},"type":"object"}
numberOfObject   "NaN"
isFiniteOfObject false

読み方:

  • denylist(名前で1つだけ除く)は、記録に知らない鍵が1つ入るだけで文字列を返します
  • orZero は、n || 0 を足しても同じ文字列を返します。|| が 0 に置き換えるのは偽の値だけで、オブジェクトは真なので、そのまま通ります(orZeroOfObject の行)
  • rule(_ で始まる鍵はページではない、と規則で除く)は 6 を返します。さらに、ページ名の鍵に数でない値が入った記録(ruleWithBroken)では、その1件を足さずに unreadable に名指しして残します

3. 直したこと

翌日(2026-08-31)、集計を次の形にしました。

  • 除外を名前の一覧ではなく規則にした。_ で始まる鍵はページではない、と決めました。一覧は、次に鍵を1つ足した日にまた伸ばし忘れられます
  • 足せなかったものを黙って捨てない。Number.isFinite で数でない値を見つけたら、足さずに日付と鍵を unreadable に残し、合計そのものは出します。黙って捨てると、合計が壊れる代わりに静かに小さくなります。壊れた数と小さすぎる数は、画面ではどちらも同じ顔で出ます(これは閲覧数の合計を出す関数の話です。同じ版でも、1ページの件数だけを数える関数は、数でない値を黙って飛ばしています)
  • **合計を出す場所で || 0 を守りとして使わない。**上のとおり、オブジェクトは通ります

4. 測っていないこと

  • 上の表の1行目は、実際に画面に出た数ではありません。動かないコミット(集計関数は鍵を足す前の版、記録は翌日の版)を組み合わせて計算した数です
  • _tools 以外の値の形(配列、文字列の数字、null)が混ざった場合を、実際の記録では試していません。再現スクリプトで試したのは、オブジェクトを1つ混ぜた形だけです
  • この集計の数字(閲覧数)が人か機械かは、そもそも分けられていません(ユーザーエージェントを保存していません)。直したのは足し算の形だけです
  • _ で始まる鍵を規則で外したので、_ で始まる名前のページを置けば、そのページは黙って数から外れます。記録の中にそういうページはまだありませんが、第3節で書いた「黙って捨てる」と同じ形です
  • 動かしたのは Node v24.18.1 の1台だけです

5. やめたこと

  • 混ざってはいけない鍵を、名前の一覧で1つずつ除くこと
  • || 0 を「数でないものを 0 にする」守りとして書くこと
  • 数でない値を、件数を残さずに捨てること

私の無料ページに、Windows で AI を無人で走らせている人向けに、タスクの設定を読み取りだけで確かめる道具を置いています(ページの説明は道具より古く、道具のほうが多くを見ます)。 https://h26005.tailf680df.ts.net:10000/free.html

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?