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?

「検出した」と「ブロックした」を同じ数値にしない:Chrome拡張のローカルレポート設計

0
Posted at

Chrome拡張のダッシュボードに大きな数字を並べると、実装者が意図していない強い主張に読まれがちです。たとえば「トラッカーイベント 151件」という表示は、151回の追跡をすべて阻止したことまで意味するのでしょうか。「リンクトラッカー 35件」は、ユーザーが35回クリックした履歴なのでしょうか。

結論から言うと、どちらも違います。この記事では、自分が開発しているMailshade 1.0.6の実装を例に、観測・保存・集計・表示上の主張を分ける設計を整理します。製品紹介ではなく、ローカルファーストなChrome拡張でレポートを作るときに再利用できる考え方が中心です。

Mailshade 1.0.6の日本語トラッカーレポート。決定論的なデモデータで、過去30日間の151トラッカーイベント、8ユニーク送信者、35リンクトラッカー、前期間比68パーセントを表示している。実際の受信箱データではない。

図1:Mailshade 1.0.6の実UI。数値と送信者名はスクリーンショット用の決定論的デモデータです。

まず、1行のイベントが何を意味するかを固定する

レポートの入力は、ローカルIndexedDBに保存したイベント行です。イベントには少なくとも次のような区別があります。

type LocalTrackerEvent = {
  id: string
  timestamp: number
  type: 'pixel' | 'link'
  client: 'gmail' | 'outlook' | 'office365' | 'superhuman' | 'yahoo' | 'protonmail'
  domain: string
  senderDomain?: string
}

重要なのは、type: 'link' を「クリック」と定義しないことです。これは、開いたメッセージ内に既知の追跡リダイレクトが存在したというローカルな検出記録です。クリック操作や遷移履歴を保存したものではありません。

同じように、イベントという名前だけで「ネットワーク要求を必ずブロックできた」とも言い切れません。既知のピクセルドメインをDNRルールで止める経路と、DOM上でリソースを検出してメッセージへ帰属させる経路は、証拠の境界が異なるからです。

集計は「選択された行」から毎回作る

レポートで期間とメールクライアントを選んだら、まずその条件に合うイベント行だけを取得します。そこから各指標を計算します。

const rows = await events
  .where('timestamp')
  .between(from, to, true, true)
  .filter(matchesSelectedClient)
  .toArray()

const trackerEvents = rows.length
const uniqueSenders = new Set(
  rows.flatMap(row => row.senderDomain ? [row.senderDomain] : [])
).size
const linkTrackers = rows.filter(row => row.type === 'link').length

このモデルなら、UIの3つの数字をそれぞれ説明できます。

表示 実際に数えているもの 数えていないもの
トラッカーイベント 選択期間・選択クライアントに一致したローカルイベント行 あらゆる追跡手法、完全なブロック成功数
ユニーク送信者 イベントに紐づいた送信者ドメインの種類 人物の一意性、メールアドレスの総数
リンクトラッカー type === 'link' のイベント行 クリック数、訪問数、遷移成功数

「何を数えたか」と同じくらい、「何を数えていないか」を仕様に書くことが大切です。

観測、最小化、集計、主張を分離する日本語の技術図。リソースURLからトラッカーのoriginとdomainだけをローカルイベントに残し、選択範囲の行を集計し、検出件数として表示する。クリック履歴や完全阻止の証明には拡張しない。

図2:観測から表示までの証拠レベル。右へ進むほど、実装が保証していない意味を足さないようにします。

元のURLをそのまま履歴に残さない

追跡リンクの判定には、パスやクエリを含むURLを一時的に扱う必要があります。しかし、レポートに必要なのは通常、追跡元のoriginまたは正規化したドメインです。そこで、保存前に情報を小さくします。

const persisted = {
  domain: parsed.hostname,
  url: `${parsed.origin}/`,
  type: 'link' as const,
}

これにより、読み取り可能なパス、クエリ、フラグメント、デコード後のリンク先をイベント履歴へ残さずに集計できます。表示のために必要な情報量と、検出時に一時的に必要な情報量を同じにしない設計です。

送信者の集計も、メッセージ本文や件名を保存しなくても、送信者ドメインと最小限の帰属情報だけで作れます。ただし、この「ローカル保存」は「データを処理していない」という意味ではありません。ローカルで扱う情報も、何を読むか、どこに残すか、どの期間で削除するかを明示すべき対象です。

前期間比は分母がないときに作らない

「前期間比 +68%」も、単純そうで境界条件があります。比較対象の前期間にイベントが0件なら、通常の増減率は分母を持ちません。

実装では次のように扱えます。

if (previousRows.length > 0) {
  change = Math.round(
    ((currentRows.length - previousRows.length) / previousRows.length) * 100
  )
} else if (currentRows.length === 0) {
  change = 0
} else {
  change = undefined
}

比較不能を0%や無限大に見せず、指標自体を出さない選択肢を持つ方が、ダッシュボードの見た目より意味の正確さを守れます。

削除と集計表は同じトランザクションで揃える

期間制限で古いイベントを削除しても、別の送信者集計表に古い件数が残れば、同じ画面の数字が矛盾します。そこで、古いイベントの削除と、残ったイベントからの送信者統計再構築を同じIndexedDBトランザクションで行います。

これは性能だけの話ではありません。ユーザーが履歴を削除したのに、要約値だけ残る状態を避けるためのプライバシー境界でもあります。

実装時のチェックリスト

レポート指標を追加するときは、次の質問を先に答えるようにしています。

  1. 1行のイベントは、観測・推論・ブロック成功のどれを表すか。
  2. 集計はどの期間、どのクライアント、どのイベント種別を含むか。
  3. 「ユニーク」は人物、アドレス、ドメインのどれか。
  4. UIのラベルから、クリック履歴や完全阻止などの強い意味が推測されないか。
  5. 元URLや本文のような、集計に不要な情報を保存していないか。
  6. 分母0、重複イベント、履歴削除後の再集計をテストしたか。
  7. デモ画面なら、実データではないことを画像説明と本文で明示したか。

まとめ

プライバシー系のダッシュボードでは、数字を大きく見せるより、数字がどの証拠から作られたかを小さく保つ方が難しいものです。

  • 検出イベントを、ブロック成功やクリックへ読み替えない
  • 必要な期間の行から指標を再計算する
  • 保存前にURLをorigin/domainへ最小化する
  • 比較不能な比率を無理に表示しない
  • 削除と要約の整合性をトランザクションで守る

この境界を先に決めておくと、あとからグラフやフィルターを増やしても、UIの主張が実装の証拠を追い越しにくくなります。

参考:Mailshade 1.0.6のリリース対応ソースとSHA-256は、公開ソースページから確認できます。


私はMailshadeを開発しているDanila Pryadkoです。この記事はAIの補助を使って下書きし、説明した挙動・型・境界条件を公開版1.0.6の実装とテスト資料で確認しました。

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?