Merge pull request #507 from jason5ng32/feat/adblock-test

Feat: Ad & Tracker Blocking Test
This commit is contained in:
Jason Ng authored and GitHub committed 2026-10-19 23:23:34 +08:00
commit 116a74b1a0
34 files changed
+1661 -3

No files matched your search

+1
View File
@@ -44,6 +44,7 @@ Feel free to bookmark the demo or deploy your own.
* 🚥 **WebRTC Detection**: Reveals the IP address exposed during WebRTC connections — including whether your browser's privacy hardening is on.
* 🛑 **DNS Leak Test**: Shows which DNS endpoints resolve your queries, to evaluate the risk of DNS leaks when using VPNs or proxies.
* 🛡️ **Ad & Tracker Blocking Test**: Checks what your ad blocker, browser, DNS filter, VPN or router actually blocks — ad networks, analytics, social trackers, error monitoring, device-maker telemetry, tracking scripts and element hiding.
* 📋 **Security Checklist**: A 258-item personal cybersecurity checklist across 12 areas, with progress saved in your browser.
### 📡 Network Tests
+1
View File
@@ -47,6 +47,7 @@ N'hésitez pas à mettre la démo en favori ou à déployer votre propre instanc
* 🚥 **Détection WebRTC** : Révèle l'adresse IP exposée lors des connexions WebRTC — y compris si les protections de confidentialité de votre navigateur sont actives.
* 🛑 **Test de fuite DNS** : Montre quels points de terminaison DNS résolvent vos requêtes, afin d'évaluer le risque de fuite DNS lors de l'utilisation d'un VPN ou d'un proxy.
* 🛡️ **Test de blocage des pubs et traceurs** : Vérifie ce que votre bloqueur de pubs, navigateur, filtre DNS, VPN ou routeur bloque vraiment — régies publicitaires, statistiques, traceurs sociaux, suivi des erreurs, télémétrie des fabricants d'appareils, scripts de pistage et masquage d'éléments.
* 📋 **Liste de contrôle de cybersécurité** : Une liste personnelle de 258 points de cybersécurité répartis en 12 domaines, avec progression sauvegardée dans votre navigateur.
### 📡 Tests réseau
+1
View File
@@ -47,6 +47,7 @@ Adicione a demonstração aos favoritos ou faça sua própria implantação.
* 🚥 **Detecção de WebRTC**: Revela o endereço IP exposto durante conexões WebRTC — inclusive se as proteções de privacidade do seu navegador estão ativas.
* 🛑 **Teste de vazamento de DNS**: Mostra quais endpoints DNS resolvem suas consultas, para avaliar o risco de vazamentos de DNS ao usar VPNs ou proxies.
* 🛡️ **Teste de bloqueio de anúncios e rastreadores**: Verifica o que seu bloqueador de anúncios, navegador, filtro de DNS, VPN ou roteador realmente bloqueia — redes de anúncios, análise, rastreadores sociais, monitoramento de erros, telemetria de fabricantes de dispositivos, scripts de rastreamento e ocultação de elementos.
* 📋 **Checklist de segurança**: Um checklist pessoal de cibersegurança com 258 itens em 12 áreas, com progresso salvo no seu navegador.
### 📡 Testes de rede
+1
View File
@@ -47,6 +47,7 @@
* 🚥 **Обнаружение WebRTC**: показывает IP-адрес, раскрываемый при подключениях WebRTC, — в том числе включена ли в вашем браузере усиленная защита приватности.
* 🛑 **Тест утечки DNS**: показывает, какие конечные точки DNS обрабатывают ваши запросы, чтобы оценить риск утечки DNS при использовании VPN или прокси.
* 🛡️ **Проверка блокировки рекламы и трекеров**: показывает, что на самом деле блокирует ваш блокировщик рекламы, браузер, DNS-фильтр, VPN или роутер, — рекламные сети, аналитику, трекеры соцсетей, мониторинг ошибок, телеметрию производителей устройств, скрипты отслеживания и скрытие элементов.
* 📋 **Контрольный список безопасности**: 258 рекомендаций по личной кибербезопасности в 12 разделах; прогресс сохраняется в браузере.
### 📡 Сетевые тесты
+1
View File
@@ -47,6 +47,7 @@
* 🚥 **WebRTC 洩漏測試**:揭示 WebRTC 連線過程中暴露的 IP 位址——包括你的瀏覽器隱私強化設定是否已開啟。
* 🛑 **DNS 洩漏測試**:顯示是哪些 DNS 端點在解析你的查詢,以評估使用 VPN 或代理時 DNS 洩漏的風險。
* 🛡️ **廣告與追蹤封鎖檢測**:檢查你的廣告阻擋器、瀏覽器、DNS 過濾、VPN 或路由器實際擋下了什麼——涵蓋廣告網路、統計分析、社群平台追蹤、錯誤監控、裝置廠商遙測、追蹤腳本與元素隱藏。
* 📋 **安全檢查清單**:一份涵蓋 12 個領域、共 258 個項目的個人網路安全檢查清單,進度儲存在你的瀏覽器裡。
### 📡 網路測試
+1
View File
@@ -47,6 +47,7 @@
* 🚥 **WebRTC 检测**:揭示 WebRTC 连接过程中暴露的 IP 地址 —— 包括你的浏览器是否开启了隐私强化。
* 🛑 **DNS 泄露测试**:展示是哪些 DNS 出口在解析你的请求,以便评估使用 VPN 或代理时 DNS 泄露的风险。
* 🛡️ **广告与跟踪屏蔽检测**:检查你的广告拦截器、浏览器、DNS 过滤、VPN 或路由器实际拦住了什么——涵盖广告网络、统计分析、社交平台跟踪、错误监控、设备厂商遥测、跟踪脚本与元素隐藏。
* 📋 **安全检查清单**:一份覆盖 12 个领域、共 258 项的个人网络安全检查清单,进度保存在你的浏览器里。
### 📡 网络测试
+13
View File
@@ -268,6 +268,19 @@ const SECTION_SPECS = {
rtt: opt(int(0, 600000)),
}))),
}),
// Ad & Tracker Blocking Test: per-category request counts only. Group
// ids stay capped strings — the probe list lives in the front end and
// changes with it.
adblocktest: obj({
testedAt: isoDate(),
groups: arr(16, obj({
id: str(16),
total: int(0, 1024),
blocked: int(0, 1024),
open: int(0, 1024),
unknown: int(0, 1024),
})),
}),
invisibility: obj({
testedAt: isoDate(),
ip: opt(ipValue()),
+3 -1
View File
@@ -125,7 +125,9 @@ masked; in the privacy policy). No backend 5xx capture; envelopes: `/api/monitor
Tailwind only when neither fits. Keep on upstream syncs: `Spinner` +
`ToolLoadingSkeleton`, `toggle` / `toggle-group`'s `primary` pressed pair, the
overlay roots' shortcut suspension, `select`'s trigger geometry (`py-1` + flex
value span, not `-webkit-box` — Safari shifts it ~2px).
value span, not `-webkit-box` — Safari shifts it ~2px). CLI trap: `shadcn-vue
add` here writes `import { cn } from "cn"` and installs the unrelated `cn`
package — drop the dependency, import from `@/lib/utils`.
### Design tokens
@@ -0,0 +1,374 @@
<template>
<div class="adblock-test-section my-4 space-y-4">
<!-- Top note: what it tests, how to read it, what leaves the browser -->
<div class="text-sm text-muted-foreground space-y-1.5 leading-relaxed">
<p>{{ t('adblocktest.Note') }}</p>
<p>{{ t('adblocktest.Note2') }}</p>
<p>{{ t('adblocktest.Note3') }}</p>
</div>
<!-- Probe picker (left) + results (right), GlobalLatencyTest's layout -->
<Card>
<CardContent class="p-0">
<div class="grid grid-cols-1 md:grid-cols-4 divide-y md:divide-y-0 md:divide-x">
<!-- Left: tally + bulk actions on top (sticky, as in the
Globalping picker), then one section per group whose
header doubles as select-all. Once results exist the
column matches their height. -->
<div class="col-span-1" :class="hasResults ? 'md:relative md:min-h-96' : ''">
<div class="max-h-72 overflow-y-auto"
:class="hasResults ? 'md:max-h-none md:absolute md:inset-0' : 'md:max-h-128'">
<div class="px-4 py-3 space-y-4">
<div
class="sticky top-0 z-10 -mt-3 pt-3 bg-card pb-1.5 flex flex-wrap items-center justify-between gap-x-3 gap-y-1 text-xs text-muted-foreground">
<span>
{{ t('adblocktest.Selected') }}
<span class="tabular-nums">{{ selectedTargetCount }}</span>
</span>
<span class="flex items-center gap-3">
<button type="button" class="cursor-pointer hover:text-foreground disabled:pointer-events-none disabled:opacity-50"
:disabled="isRunning" @click="selectAll">
{{ t('adblocktest.SelectAll') }}
</button>
<button type="button" class="cursor-pointer hover:text-foreground disabled:pointer-events-none disabled:opacity-50"
:disabled="isRunning" @click="invert">
{{ t('adblocktest.Invert') }}
</button>
</span>
</div>
<div v-for="group in ADBLOCK_PROBE_GROUPS" :key="group.id" class="space-y-2">
<label class="flex items-center gap-2 text-sm font-medium cursor-pointer">
<Checkbox :model-value="groupState(group)" :disabled="isRunning"
@update:model-value="(v) => toggleGroup(group, v)">
<Minus v-if="groupState(group) === 'indeterminate'" class="size-3" />
<Check v-else class="size-3.5" />
</Checkbox>
{{ t(`adblocktest.group.${group.id}.title`) }}
</label>
<div class="flex flex-col gap-1.5 pl-6">
<label v-for="vendor in group.vendors" :key="vendor.id"
class="flex items-center gap-2 text-sm min-w-0"
:class="isRunning ? 'opacity-50' : 'cursor-pointer'">
<Checkbox :model-value="isSelected(group, vendor)" :disabled="isRunning"
@update:model-value="(v) => setVendor(group, vendor, v)" />
<span class="truncate">{{ vendorName(group, vendor) }}</span>
</label>
</div>
</div>
</div>
</div>
</div>
<!-- Right: toolbar (filter tags / hint + run), then results -->
<div class="col-span-3 min-w-0 flex flex-col">
<div class="flex flex-wrap items-center justify-between gap-3 px-4 py-3 border-b">
<ToggleGroup v-if="hasResults" :model-value="filter" type="single" variant="outline"
:spacing="2" class="flex-wrap justify-start"
@update:model-value="(v) => v && (filter = v)">
<ToggleGroupItem v-for="f in FILTERS" :key="f" :value="f" :class="tagClass">
{{ t(`adblocktest.filter.${f}`) }}
</ToggleGroupItem>
</ToggleGroup>
<span v-else class="text-sm text-muted-foreground">{{ t('adblocktest.Hint') }}</span>
<Button variant="action" :disabled="!canRun" class="cursor-pointer ml-auto" @click="startTest">
<Spinner v-if="isRunning" />
<Play v-else class="size-4 shrink-0" />
{{ t('adblocktest.Run') }}
</Button>
</div>
<div v-if="hasResults" class="p-4 space-y-5">
<!-- Summary: score, counts, where the blocking happens -->
<div class="space-y-3">
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
<div class="rounded-md bg-muted/50 px-3 py-2">
<div class="text-xs text-muted-foreground">{{ t('adblocktest.Score') }}</div>
<div class="text-2xl font-semibold tabular-nums">
{{ requestTally.score ?? '—' }}<span class="text-base">%</span>
</div>
</div>
<div v-for="key in STATUS_KEYS" :key="key" class="rounded-md bg-muted/50 px-3 py-2">
<div class="flex items-center gap-1.5 text-xs" :class="textClass(statusTone(key))">
<component :is="STATUS_ICON[key]" class="size-3.5 shrink-0" />
{{ t(`adblocktest.count.${key}`) }}
</div>
<div class="text-2xl font-semibold tabular-nums"
:class="requestTally[key] > 0 ? textClass(statusTone(key)) : 'text-muted-foreground'">
{{ requestTally[key] }}
</div>
</div>
</div>
<p v-if="isRunning" class="flex items-center gap-2 text-sm text-muted-foreground">
<Spinner class="size-4" />
{{ t('adblocktest.Progress', { done: answeredCount, total: runPlan.length }) }}
</p>
<Alert v-else-if="layer">
<Info />
<AlertDescription>{{ t(`adblocktest.layer.${layer}`) }}</AlertDescription>
</Alert>
</div>
<!-- One block per tested group -->
<section v-for="group in visibleGroups" :key="group.id" class="space-y-2">
<header class="flex flex-wrap items-baseline justify-between gap-x-4 gap-y-0.5">
<h3 class="text-sm font-semibold m-0">{{ t(`adblocktest.group.${group.id}.title`) }}</h3>
<span class="text-xs text-muted-foreground tabular-nums">
{{ t('adblocktest.GroupCount', { blocked: group.counts.blocked, total: group.counts.total }) }}
</span>
<p class="basis-full text-xs text-muted-foreground m-0">
{{ t(`adblocktest.group.${group.id}.desc`) }}
</p>
</header>
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-2">
<div v-for="vendor in group.vendors" :key="vendor.key"
class="rounded-lg border bg-card min-w-0">
<!-- Tile header: name + a count per status (no blended
color); toggles the per-address list -->
<button type="button"
class="flex w-full items-center gap-2 px-3 py-2 text-left text-sm cursor-pointer"
:aria-expanded="isExpanded(vendor)"
@click="toggleExpanded(vendor)">
<span class="truncate flex-1 font-medium">{{ vendor.name }}</span>
<Spinner v-if="isRunning && vendor.counts.pending > 0"
class="size-3.5 text-muted-foreground" />
<template v-for="key in STATUS_KEYS" :key="key">
<span v-if="vendor.counts[key] > 0"
class="flex items-center gap-0.5 text-xs tabular-nums shrink-0"
:class="textClass(statusTone(key))"
:title="t(`adblocktest.status.${key}`)">
<component :is="STATUS_ICON[key]" class="size-3.5" />
{{ vendor.counts[key] }}
</span>
</template>
<ChevronDown class="size-3.5 shrink-0 text-muted-foreground transition-transform"
:class="isExpanded(vendor) ? 'rotate-180' : ''" />
</button>
<ul v-if="isExpanded(vendor)" class="border-t px-3 py-2 space-y-1">
<li v-for="item in vendor.items" :key="item.id"
class="flex items-center gap-2 text-xs min-w-0"
:title="item.url ? `${item.url} · ${t(`adblocktest.status.${statusOf(item)}`)}` : ''">
<Spinner v-if="statusOf(item) === 'pending'" class="size-3.5 text-muted-foreground" />
<component v-else :is="STATUS_ICON[statusOf(item)]" class="size-3.5 shrink-0"
:class="textClass(statusTone(statusOf(item)))" />
<span v-if="item.kind === 'cosmetic'">
{{ t(`adblocktest.cosmeticStatus.${statusOf(item)}`) }}
</span>
<span v-else class="font-mono truncate">{{ item.target }}</span>
</li>
</ul>
</div>
</div>
</section>
<p v-if="!visibleGroups.length" class="text-sm text-muted-foreground text-center py-4">
{{ t(filter === 'blocked' ? 'adblocktest.NothingBlocked' : 'adblocktest.NothingOpen') }}
</p>
</div>
</div>
</div>
</CardContent>
</Card>
<!-- Probe list version + what the probes cannot tell -->
<div v-if="hasResults" class="text-xs text-muted-foreground m-0 space-y-1">
<p class="text-xs text-muted-foreground m-0">
{{ t('adblocktest.Version', { version: ADBLOCK_PROBES_VERSION }) }}
</p>
<p class="text-xs text-muted-foreground mt-1">{{ t('adblocktest.Limitation') }}</p>
</div>
<!-- Cosmetic baits live here during a run, off-screen but laid out -->
<div ref="baitHost" aria-hidden="true" class="pointer-events-none fixed left-[-10000px] top-0 w-24"></div>
</div>
</template>
<script setup>
// Ad & Tracker Blocking Test (/tools/adblocktest): checks what the visitor's
// blocker — extension, browser, DNS service, VPN or router — lets through.
// Nothing runs on open; every probe group starts selected. Pure logic lives
// in utils/features/adblock-test.js, the probe list in data/adblock-probes.js.
import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { trackEvent } from '@/utils/analytics';
import { emitAppEvent } from '@/utils/app-events.js';
import { ADBLOCK_PROBE_GROUPS, ADBLOCK_PROBES_VERSION } from '@/data/adblock-probes.js';
import {
COSMETIC_BAITS, COSMETIC_POLL_MS, COSMETIC_WAIT_MS,
allVendorKeys, blockingLayer, buildPlan, invertSelection, isBaitHidden,
probeLimits, probeRequest, runWithConcurrency, statusTone, tally, vendorKey,
} from '@/utils/features/adblock-test.js';
import { useStatusTone } from '@/composables/use-status-tone.js';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Checkbox } from '@/components/ui/checkbox';
import { Spinner } from '@/components/ui/spinner';
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
import {
Ban, Check, ChevronDown, CircleArrowRight, CircleQuestionMark, Info, Minus, Play,
} from '@lucide/vue';
const { t } = useI18n();
const { textClass } = useStatusTone();
const tagClass = 'group h-7 rounded-full px-2.5 text-xs cursor-pointer';
const FILTERS = ['all', 'open', 'blocked'];
// Answered statuses, in display order (tiles, tile counts).
const STATUS_KEYS = ['blocked', 'open', 'unknown'];
const STATUS_ICON = { blocked: Ban, open: CircleArrowRight, unknown: CircleQuestionMark };
/* ---------- Selection (left column) ---------- */
const ALL_KEYS = allVendorKeys(ADBLOCK_PROBE_GROUPS);
const selected = ref(new Set(ALL_KEYS));
const isSelected = (group, vendor) => selected.value.has(vendorKey(group.id, vendor.id));
const setVendor = (group, vendor, checked) => {
const next = new Set(selected.value);
if (checked) next.add(vendorKey(group.id, vendor.id));
else next.delete(vendorKey(group.id, vendor.id));
selected.value = next;
};
const groupState = (group) => {
const on = group.vendors.filter((v) => isSelected(group, v)).length;
if (on === group.vendors.length) return true;
return on > 0 ? 'indeterminate' : false;
};
const toggleGroup = (group, checked) => {
const next = new Set(selected.value);
for (const v of group.vendors) {
if (checked === true) next.add(vendorKey(group.id, v.id));
else next.delete(vendorKey(group.id, v.id));
}
selected.value = next;
};
const selectAll = () => { selected.value = new Set(ALL_KEYS); };
const invert = () => { selected.value = new Set(invertSelection(ALL_KEYS, selected.value)); };
const vendorName = (group, vendor) => (group.kind === 'cosmetic'
? t(`adblocktest.cosmetic.${vendor.id}`)
: vendor.name);
const selectedTargetCount = computed(() => buildPlan(ADBLOCK_PROBE_GROUPS, selected.value).length);
/* ---------- Run state ---------- */
// 'idle' | 'running' | 'finished'
const status = ref('idle');
const isRunning = computed(() => status.value === 'running');
const canRun = computed(() => !isRunning.value && selectedTargetCount.value > 0);
const runPlan = ref([]);
const statuses = ref({}); // plan item id → probe status
const hasResults = computed(() => runPlan.value.length > 0);
const statusOf = (item) => statuses.value[item.id] ?? 'pending';
const requestItems = computed(() => runPlan.value.filter((p) => p.kind !== 'cosmetic'));
const cosmeticItems = computed(() => runPlan.value.filter((p) => p.kind === 'cosmetic'));
const requestTally = computed(() => tally(requestItems.value.map(statusOf)));
const cosmeticTally = computed(() => tally(cosmeticItems.value.map(statusOf)));
const answeredCount = computed(() => runPlan.value.length
- requestTally.value.pending - cosmeticTally.value.pending);
const layer = computed(() => (status.value === 'finished'
? blockingLayer({ requests: requestTally.value, cosmetic: cosmeticTally.value })
: null));
/* ---------- Results (right column) ---------- */
const filter = ref('all');
const expanded = ref(new Set());
const isExpanded = (vendor) => filter.value !== 'all' || expanded.value.has(vendor.key);
const toggleExpanded = (vendor) => {
const next = new Set(expanded.value);
if (next.has(vendor.key)) next.delete(vendor.key);
else next.add(vendor.key);
expanded.value = next;
};
// Groups → vendors → items of the last run, with counts; the "open" /
// "blocked" filters keep only items of that status (and drop what is left empty).
const visibleGroups = computed(() => ADBLOCK_PROBE_GROUPS.map((group) => {
const vendors = group.vendors.map((vendor) => {
const all = runPlan.value.filter((p) => p.groupId === group.id && p.vendorId === vendor.id);
const items = filter.value === 'all' ? all : all.filter((p) => statusOf(p) === filter.value);
return {
key: vendorKey(group.id, vendor.id),
name: vendorName(group, vendor),
items,
counts: tally(all.map(statusOf)),
};
}).filter((v) => v.items.length > 0);
const all = runPlan.value.filter((p) => p.groupId === group.id);
return { id: group.id, vendors, counts: tally(all.map(statusOf)) };
}).filter((g) => g.vendors.length > 0));
/* ---------- Probes ---------- */
const baitHost = ref(null);
// Place every selected bait, then watch until each is hidden or the wait
// runs out. A host no longer in the document (the page was left mid-run)
// proves nothing, so its baits read unknown.
const runCosmetic = async (items) => {
const host = baitHost.value;
if (!items.length || !host) return;
const baits = items.map((item) => {
const spec = COSMETIC_BAITS[item.target];
const el = document.createElement(spec.tag);
el.className = spec.className;
for (const [k, v] of Object.entries(spec.attrs)) el.setAttribute(k, v);
el.style.cssText = 'display:block;width:80px;height:20px;';
el.innerHTML = '&nbsp;';
host.appendChild(el);
return { item, el };
});
const hidden = (el) => {
const style = getComputedStyle(el);
return isBaitHidden({
connected: el.isConnected,
offsetHeight: el.offsetHeight,
display: style.display,
visibility: style.visibility,
});
};
const started = Date.now();
let pending = baits;
while (pending.length && Date.now() - started < COSMETIC_WAIT_MS) {
await new Promise((resolve) => setTimeout(resolve, COSMETIC_POLL_MS));
if (!host.isConnected) break;
pending = pending.filter(({ item, el }) => {
if (!hidden(el)) return true;
statuses.value[item.id] = 'blocked';
return false;
});
}
for (const { item } of pending) statuses.value[item.id] = host.isConnected ? 'open' : 'unknown';
host.replaceChildren();
};
const startTest = async () => {
if (!canRun.value) return;
trackEvent('Section', 'StartClick', 'AdblockTest');
status.value = 'running';
filter.value = 'all';
expanded.value = new Set();
const plan = buildPlan(ADBLOCK_PROBE_GROUPS, selected.value);
runPlan.value = plan;
statuses.value = {};
const { timeoutMs, concurrency } = probeLimits(navigator.userAgent, navigator.platform);
const tasks = plan.filter((p) => p.kind !== 'cosmetic').map((item) => async () => {
statuses.value[item.id] = await probeRequest(item.url, { timeoutMs });
});
await Promise.all([
runCosmetic(plan.filter((p) => p.kind === 'cosmetic')),
runWithConcurrency(tasks, concurrency),
]);
status.value = 'finished';
// Domain event: per-address outcome for the report collector.
emitAppEvent('adblocktest:finished', {
items: plan.map((p) => ({ groupId: p.groupId, kind: p.kind, status: statusOf(p) })),
});
};
</script>
@@ -85,6 +85,7 @@ import ReportPingtest from './sections/ReportPingtest.vue';
import ReportMtrtest from './sections/ReportMtrtest.vue';
import ReportRuletest from './sections/ReportRuletest.vue';
import ReportBrowserinfo from './sections/ReportBrowserinfo.vue';
import ReportAdblocktest from './sections/ReportAdblocktest.vue';
import ReportInvisibility from './sections/ReportInvisibility.vue';
import ReportEnhanceddnsleak from './sections/ReportEnhanceddnsleak.vue';
import ReportPersona from './sections/ReportPersona.vue';
@@ -100,6 +101,7 @@ const SECTION_COMPONENTS = {
mtrtest: ReportMtrtest,
ruletest: ReportRuletest,
browserinfo: ReportBrowserinfo,
adblocktest: ReportAdblocktest,
invisibility: ReportInvisibility,
enhanceddnsleak: ReportEnhanceddnsleak,
persona: ReportPersona,
@@ -0,0 +1,47 @@
<template>
<div class="overflow-x-auto">
<table class="w-full text-xs">
<thead>
<tr class="border-b">
<th scope="col" :class="TH_LEFT">{{ t('adblocktest.report.Category') }}</th>
<th v-for="key in STATUS_KEYS" :key="key" scope="col" :class="TH_RIGHT">
{{ t(`adblocktest.count.${key}`) }}
</th>
<th scope="col" :class="TH_RIGHT">{{ t('adblocktest.report.Total') }}</th>
</tr>
</thead>
<tbody class="divide-y">
<tr v-for="group in section.groups" :key="group.id" class="hover:bg-muted/50 transition-colors">
<td class="px-3 py-2">{{ groupTitle(group.id) }}</td>
<td v-for="key in STATUS_KEYS" :key="key"
:class="[TD_NUM, group[key] > 0 ? textClass(statusTone(key)) : 'text-muted-foreground']">
{{ group[key] }}
</td>
<td :class="[TD_NUM, 'text-muted-foreground']">{{ group.total }}</td>
</tr>
</tbody>
</table>
</div>
</template>
<script setup>
// Read-only renderer for the Ad & Tracker Blocking Test report section: the
// request counts per category. Titles come from the tool's own i18n entries;
// an id the current list no longer knows renders as-is.
import { useI18n } from 'vue-i18n';
import { statusTone } from '@/utils/features/adblock-test.js';
import { useStatusTone } from '@/composables/use-status-tone.js';
import { TH_LEFT, TH_RIGHT, TD_NUM } from './table-classes.js';
defineProps({ section: { type: Object, required: true } });
const { t, te } = useI18n();
const { textClass } = useStatusTone();
const STATUS_KEYS = ['blocked', 'open', 'unknown'];
const groupTitle = (id) => {
const key = `adblocktest.group.${id}.title`;
return te(key) ? t(key) : id;
};
</script>
@@ -1,6 +1,6 @@
// Shared Tailwind class strings for the report tables — same visual language
// as the ping-test result table (GlobalLatencyTest.vue). Kept as constants so
// the eleven renderers stay literally consistent.
// the renderers stay literally consistent.
export const TH_LEFT = 'px-3 py-2.5 text-xs font-medium text-muted-foreground uppercase tracking-wide text-left text-nowrap';
export const TH_RIGHT = 'px-3 py-2.5 text-xs font-medium text-muted-foreground uppercase tracking-wide text-right text-nowrap';
+19
View File
@@ -0,0 +1,19 @@
<script setup>
import { cn } from "@/lib/utils";
import { alertVariants } from ".";
const props = defineProps({
class: { type: null, required: false },
variant: { type: null, required: false },
});
</script>
<template>
<div
data-slot="alert"
:class="cn(alertVariants({ variant }), props.class)"
role="alert"
>
<slot />
</div>
</template>
@@ -0,0 +1,21 @@
<script setup>
import { cn } from "@/lib/utils";
const props = defineProps({
class: { type: null, required: false },
});
</script>
<template>
<div
data-slot="alert-description"
:class="
cn(
'text-muted-foreground col-start-2 text-sm [&_p]:leading-relaxed',
props.class,
)
"
>
<slot />
</div>
</template>
@@ -0,0 +1,21 @@
<script setup>
import { cn } from "@/lib/utils";
const props = defineProps({
class: { type: null, required: false },
});
</script>
<template>
<div
data-slot="alert-title"
:class="
cn(
'col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight',
props.class,
)
"
>
<slot />
</div>
</template>
+21
View File
@@ -0,0 +1,21 @@
import { cva } from "class-variance-authority";
export { default as Alert } from "./Alert.vue";
export { default as AlertDescription } from "./AlertDescription.vue";
export { default as AlertTitle } from "./AlertTitle.vue";
export const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
{
variants: {
variant: {
default: "bg-card text-card-foreground",
destructive:
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
},
},
defaultVariants: {
variant: "default",
},
},
);
+1
View File
@@ -172,6 +172,7 @@ const buildShortcutConfig = ({ refs, store, t, configs, userPreferences }) => {
{ keys: 'n', action: () => goToAdvancedTool('dnsresolver', 'DNSResolver'), description: t('shortcutKeys.DNSResolver') },
{ keys: 'C', action: () => goToAdvancedTool('censorshipcheck', 'CensorshipCheck'), description: t('shortcutKeys.CensorshipCheck') },
{ keys: 'b', action: () => goToAdvancedTool('browserinfo', 'BrowserInfo'), description: t('shortcutKeys.BrowserInfo') },
{ keys: 'x', action: () => goToAdvancedTool('adblocktest', 'AdblockTest'), description: t('shortcutKeys.AdblockTest') },
{ keys: 'W', action: () => goToAdvancedTool('whois', 'Whois'), description: t('shortcutKeys.Whois') },
{ keys: 'v', action: () => goToAdvancedTool('servicestatus', 'ServiceStatus'), description: t('shortcutKeys.ServiceStatus') },
{
+157
View File
@@ -0,0 +1,157 @@
// Static probe list for the Ad & Tracker Blocking Test (AdblockTest.vue).
//
// Groups render in this order, each a set of vendors:
// kind 'domain' — `targets` are hostnames, probed at https://<host>/ ; a
// hit here is what DNS / VPN / router filters catch too
// kind 'script' — `targets` are full URLs of well-known tag / pixel
// scripts, fetched (never executed)
// kind 'cosmetic' — bait elements on the page itself; `targets` name the
// bait ids in utils/features/adblock-test.js
//
// Only widely used services make the list. Every target must resolve on
// public DNS and answer HTTPS with a valid certificate, without redirecting
// to http:// or in a loop: any of those fails like a block and reads as
// "blocked". Bump ADBLOCK_PROBES_VERSION whenever the list changes.
export const ADBLOCK_PROBES_VERSION = '2026-10-19';
export const ADBLOCK_PROBE_GROUPS = [
{
id: 'ads',
kind: 'domain',
vendors: [
{ id: 'google-ads', name: 'Google Ads', targets: ['pagead2.googlesyndication.com', 'adservice.google.com', 'googleads.g.doubleclick.net', 'tpc.googlesyndication.com'] },
{ id: 'doubleclick', name: 'DoubleClick', targets: ['ad.doubleclick.net', 'static.doubleclick.net', 'stats.g.doubleclick.net', 'securepubads.g.doubleclick.net'] },
{ id: 'amazon-ads', name: 'Amazon Ads', targets: ['aax.amazon-adsystem.com', 'c.amazon-adsystem.com', 's.amazon-adsystem.com'] },
{ id: 'microsoft-ads', name: 'Microsoft Advertising', targets: ['ads.microsoft.com'] },
{ id: 'criteo', name: 'Criteo', targets: ['static.criteo.net', 'bidder.criteo.com', 'gum.criteo.com'] },
{ id: 'taboola', name: 'Taboola', targets: ['cdn.taboola.com', 'trc.taboola.com'] },
{ id: 'outbrain', name: 'Outbrain', targets: ['widgets.outbrain.com', 'log.outbrain.com'] },
{ id: 'media-net', name: 'Media.net', targets: ['static.media.net', 'contextual.media.net'] },
{ id: 'trade-desk', name: 'The Trade Desk', targets: ['js.adsrvr.org', 'insight.adsrvr.org', 'match.adsrvr.org'] },
{ id: 'magnite', name: 'Magnite', targets: ['fastlane.rubiconproject.com', 'pixel.rubiconproject.com'] },
{ id: 'pubmatic', name: 'PubMatic', targets: ['ads.pubmatic.com', 'image6.pubmatic.com'] },
{ id: 'adroll', name: 'AdRoll', targets: ['d.adroll.com', 's.adroll.com'] },
{ id: 'yahoo', name: 'Yahoo', targets: ['ads.yahoo.com', 'analytics.yahoo.com', 'gemini.yahoo.com'] },
{ id: 'applovin', name: 'AppLovin', targets: ['d.applovin.com', 'ms.applovin.com', 'rt.applovin.com'] },
{ id: 'unity-ads', name: 'Unity Ads', targets: ['auction.unityads.unity3d.com', 'config.unityads.unity3d.com', 'adserver.unityads.unity3d.com'] },
{ id: 'ironsource', name: 'ironSource', targets: ['outcome-ssp.supersonicads.com', 'init.supersonicads.com'] },
{ id: 'pangle', name: 'Pangle', targets: ['api16-access-sg.pangle.io', 'pangolin16.isnssdk.com'] },
],
},
{
id: 'analytics',
kind: 'domain',
vendors: [
{ id: 'google-analytics', name: 'Google Analytics', targets: ['www.google-analytics.com', 'ssl.google-analytics.com', 'region1.google-analytics.com', 'analytics.google.com'] },
{ id: 'gtm', name: 'Google Tag Manager', targets: ['www.googletagmanager.com'] },
{ id: 'firebase-analytics', name: 'Firebase Analytics', targets: ['app-measurement.com', 'firebaselogging-pa.googleapis.com'] },
{ id: 'clarity', name: 'Microsoft Clarity', targets: ['www.clarity.ms', 'c.clarity.ms'] },
{ id: 'hotjar', name: 'Hotjar', targets: ['static.hotjar.com', 'script.hotjar.com', 'insights.hotjar.com'] },
{ id: 'adobe-analytics', name: 'Adobe Analytics', targets: ['assets.adobedtm.com', 'dpm.demdex.net'] },
{ id: 'mixpanel', name: 'Mixpanel', targets: ['api-js.mixpanel.com', 'cdn.mxpnl.com'] },
{ id: 'segment', name: 'Segment', targets: ['api.segment.io', 'cdn.segment.com'] },
{ id: 'amplitude', name: 'Amplitude', targets: ['api2.amplitude.com', 'cdn.amplitude.com'] },
{ id: 'fullstory', name: 'FullStory', targets: ['edge.fullstory.com', 'rs.fullstory.com'] },
{ id: 'posthog', name: 'PostHog', targets: ['us.i.posthog.com', 'eu.i.posthog.com'] },
{ id: 'plausible', name: 'Plausible', targets: ['plausible.io'] },
{ id: 'cloudflare-analytics', name: 'Cloudflare Web Analytics', targets: ['cloudflareinsights.com'] },
{ id: 'yandex-metrica', name: 'Yandex Metrica', targets: ['mc.yandex.ru', 'mc.yandex.com'] },
{ id: 'comscore', name: 'Comscore', targets: ['sb.scorecardresearch.com', 'b.scorecardresearch.com'] },
{ id: 'adjust', name: 'Adjust', targets: ['app.adjust.com'] },
{ id: 'appsflyer', name: 'AppsFlyer', targets: ['t.appsflyer.com', 'events.appsflyer.com'] },
{ id: 'branch', name: 'Branch', targets: ['api2.branch.io', 'cdn.branch.io'] },
],
},
{
id: 'social',
kind: 'domain',
vendors: [
{ id: 'meta', name: 'Meta', targets: ['connect.facebook.net', 'pixel.facebook.com', 'an.facebook.com'] },
{ id: 'x', name: 'X (Twitter)', targets: ['static.ads-twitter.com', 'ads-api.twitter.com', 'analytics.twitter.com'] },
{ id: 'linkedin', name: 'LinkedIn', targets: ['px.ads.linkedin.com', 'snap.licdn.com'] },
{ id: 'pinterest', name: 'Pinterest', targets: ['ct.pinterest.com', 'log.pinterest.com', 'trk.pinterest.com'] },
{ id: 'reddit', name: 'Reddit', targets: ['events.redditmedia.com', 'alb.reddit.com'] },
{ id: 'tiktok', name: 'TikTok', targets: ['analytics.tiktok.com', 'analytics-sg.tiktok.com', 'ads.tiktok.com'] },
{ id: 'snapchat', name: 'Snapchat', targets: ['tr.snapchat.com', 'sc-static.net'] },
{ id: 'vk', name: 'VK / Mail.ru', targets: ['top-fwz1.mail.ru'] },
{ id: 'line', name: 'LINE', targets: ['tr.line.me', 'd.line-scdn.net'] },
],
},
{
id: 'errors',
kind: 'domain',
vendors: [
{ id: 'sentry', name: 'Sentry', targets: ['browser.sentry-cdn.com', 'o1.ingest.sentry.io', 'js.sentry-cdn.com'] },
{ id: 'bugsnag', name: 'Bugsnag', targets: ['notify.bugsnag.com', 'sessions.bugsnag.com'] },
{ id: 'new-relic', name: 'New Relic', targets: ['js-agent.newrelic.com', 'bam.nr-data.net'] },
{ id: 'datadog', name: 'Datadog RUM', targets: ['browser-intake-datadoghq.com', 'www.datadoghq-browser-agent.com'] },
{ id: 'dynatrace', name: 'Dynatrace', targets: ['js-cdn.dynatrace.com'] },
{ id: 'rollbar', name: 'Rollbar', targets: ['api.rollbar.com', 'cdn.rollbar.com'] },
{ id: 'crashlytics', name: 'Firebase Crashlytics', targets: ['crashlyticsreports-pa.googleapis.com'] },
],
},
{
id: 'oem',
kind: 'domain',
vendors: [
{ id: 'apple', name: 'Apple', targets: ['iadsdk.apple.com', 'metrics.icloud.com', 'api-adservices.apple.com', 'weather-analytics-events.apple.com'] },
{ id: 'windows', name: 'Microsoft Windows', targets: ['vortex.data.microsoft.com'] },
{ id: 'samsung', name: 'Samsung', targets: ['samsungads.com', 'smetrics.samsung.com', 'samsung-com.112.2o7.net', 'log-config.samsungacr.com'] },
{ id: 'xiaomi', name: 'Xiaomi', targets: ['api.ad.xiaomi.com', 'data.mistat.xiaomi.com', 'tracking.miui.com', 'data.mistat.intl.xiaomi.com'] },
{ id: 'huawei', name: 'Huawei', targets: ['metrics.data.hicloud.com', 'metrics2.data.hicloud.com', 'logservice.hicloud.com'] },
{ id: 'oppo', name: 'OPPO / realme', targets: ['adsfs.oppomobile.com', 'adx.ads.oppomobile.com', 'data.ads.oppomobile.com', 'bdapi-ads.realmemobile.com'] },
{ id: 'vivo', name: 'vivo', targets: ['adsdk.vivo.com.cn'] },
{ id: 'oneplus', name: 'OnePlus', targets: ['open.oneplus.net'] },
{ id: 'lg', name: 'LG', targets: ['ngfts.lge.com', 'us.ad.lgsmartad.com', 'us.info.lgsmartad.com', 'alphonso.tv'] },
{ id: 'roku', name: 'Roku', targets: ['ads.roku.com'] },
{ id: 'amazon-devices', name: 'Amazon devices', targets: ['device-metrics-us.amazon.com', 'device-metrics-us-2.amazon.com', 'mads-eu.amazon.com'] },
],
},
{
id: 'scripts',
kind: 'script',
vendors: [
{ id: 'gtm', name: 'Google Tag Manager', targets: ['https://www.googletagmanager.com/gtag/js'] },
{ id: 'ga', name: 'Google Analytics', targets: ['https://www.google-analytics.com/analytics.js'] },
{ id: 'adsense', name: 'Google AdSense', targets: ['https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js'] },
{ id: 'gpt', name: 'Google Publisher Tag', targets: ['https://securepubads.g.doubleclick.net/tag/js/gpt.js'] },
{ id: 'meta-pixel', name: 'Meta Pixel', targets: ['https://connect.facebook.net/en_US/fbevents.js'] },
{ id: 'clarity', name: 'Microsoft Clarity', targets: ['https://www.clarity.ms/tag/uet'] },
{ id: 'uet', name: 'Microsoft UET', targets: ['https://bat.bing.com/bat.js'] },
{ id: 'hotjar', name: 'Hotjar', targets: ['https://static.hotjar.com/c/hotjar-1.js'] },
{ id: 'linkedin-insight', name: 'LinkedIn Insight', targets: ['https://snap.licdn.com/li.lms-analytics/insight.min.js'] },
{ id: 'x-pixel', name: 'X Pixel', targets: ['https://static.ads-twitter.com/uwt.js'] },
{ id: 'pinterest-tag', name: 'Pinterest Tag', targets: ['https://s.pinimg.com/ct/core.js'] },
{ id: 'reddit-pixel', name: 'Reddit Pixel', targets: ['https://www.redditstatic.com/ads/pixel.js'] },
{ id: 'snap-pixel', name: 'Snap Pixel', targets: ['https://sc-static.net/scevent.min.js'] },
{ id: 'apstag', name: 'Amazon Publisher Services', targets: ['https://c.amazon-adsystem.com/aax2/apstag.js'] },
{ id: 'criteo', name: 'Criteo', targets: ['https://static.criteo.net/js/ld/publishertag.js'] },
{ id: 'taboola', name: 'Taboola', targets: ['https://cdn.taboola.com/libtrc/unip/loader.js'] },
{ id: 'outbrain', name: 'Outbrain', targets: ['https://widgets.outbrain.com/outbrain.js'] },
{ id: 'adroll', name: 'AdRoll', targets: ['https://s.adroll.com/j/roundtrip.js'] },
{ id: 'adobe', name: 'Adobe Analytics', targets: ['https://assets.adobedtm.com/extensions/x/AppMeasurement.min.js'] },
{ id: 'segment', name: 'Segment', targets: ['https://cdn.segment.com/analytics.js/v1/x/analytics.min.js'] },
{ id: 'mixpanel', name: 'Mixpanel', targets: ['https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js'] },
{ id: 'amplitude', name: 'Amplitude', targets: ['https://cdn.amplitude.com/libs/analytics-browser-2.11.1-min.js.gz'] },
{ id: 'fullstory', name: 'FullStory', targets: ['https://edge.fullstory.com/s/fs.js'] },
{ id: 'posthog', name: 'PostHog', targets: ['https://us-assets.i.posthog.com/static/array.js'] },
{ id: 'plausible', name: 'Plausible', targets: ['https://plausible.io/js/script.js'] },
{ id: 'cloudflare-analytics', name: 'Cloudflare Web Analytics', targets: ['https://static.cloudflareinsights.com/beacon.min.js'] },
{ id: 'yandex-metrica', name: 'Yandex Metrica', targets: ['https://mc.yandex.ru/metrika/tag.js'] },
{ id: 'comscore', name: 'Comscore', targets: ['https://sb.scorecardresearch.com/beacon.js'] },
{ id: 'sentry', name: 'Sentry', targets: ['https://browser.sentry-cdn.com/8.0.0/bundle.min.js'] },
{ id: 'bugsnag', name: 'Bugsnag', targets: ['https://d2wy8f7a9ursnm.cloudfront.net/v8/bugsnag.min.js'] },
{ id: 'new-relic', name: 'New Relic', targets: ['https://js-agent.newrelic.com/nr-loader-spa-current.min.js'] },
{ id: 'datadog', name: 'Datadog RUM', targets: ['https://www.datadoghq-browser-agent.com/us1/v5/datadog-rum.js'] },
],
},
{
id: 'cosmetic',
kind: 'cosmetic',
vendors: [
{ id: 'ad-container', targets: ['ad-container'] },
{ id: 'adsense-slot', targets: ['adsense-slot'] },
],
},
];
+11
View File
@@ -2224,6 +2224,17 @@
"pt-BR": "Nova ferramenta: Verificação de listas negras de IP — veja se um IP está em cerca de 80 DNSBLs em tempo real e listas de inteligência de ameaças"
}
},
{
"type": "add",
"change": {
"en": "New tool: Ad & Tracker Blocking Test — see which ad, analytics, social and device-maker endpoints, tracking scripts and ad-shaped elements your blocker, DNS filter or VPN lets through",
"zh": "新增工具:广告与跟踪屏蔽检测——查看你的拦截器、DNS 过滤或 VPN 放行了哪些广告、统计、社交平台与设备厂商端点、跟踪脚本和广告样式元素",
"zh-TW": "新增工具:廣告與追蹤封鎖檢測——查看你的阻擋器、DNS 過濾或 VPN 放行了哪些廣告、統計、社群平台與裝置廠商端點、追蹤腳本和廣告樣式元素",
"fr": "Nouvel outil : Test de blocage des pubs et traceurs — voyez quelles adresses publicitaires, de statistiques, de réseaux sociaux et de fabricants d'appareils, quels scripts de pistage et quels éléments publicitaires votre bloqueur, filtre DNS ou VPN laisse passer",
"ru": "Новый инструмент: проверка блокировки рекламы и трекеров — узнайте, какие адреса рекламных сетей, аналитики, соцсетей и производителей устройств, скрипты отслеживания и рекламные элементы пропускает ваш блокировщик, DNS-фильтр или VPN",
"pt-BR": "Nova ferramenta: Teste de bloqueio de anúncios e rastreadores — veja quais endereços de anúncios, análise, redes sociais e fabricantes de dispositivos, scripts de rastreamento e elementos com cara de anúncio seu bloqueador, filtro de DNS ou VPN deixa passar"
}
},
{
"type": "improve",
"change": {
+1
View File
@@ -25,6 +25,7 @@ export const ADVANCED_TOOLS = [
{ slug: 'servicestatus', emoji: '📡', titleKey: 'serviceStatus.Title', noteKey: 'advancedtools.ServiceStatus', category: 'network', component: () => import('@/components/advanced-tools/ServiceStatus.vue') },
{ slug: 'browserinfo', emoji: '🖥️', titleKey: 'browserinfo.Title', noteKey: 'advancedtools.BrowserInfo', category: 'network', component: () => import('@/components/advanced-tools/BrowserInfo.vue') },
{ slug: 'securitychecklist', emoji: '📋', titleKey: 'securitychecklist.Title', noteKey: 'advancedtools.SecurityChecklist', category: 'network', component: () => import('@/components/advanced-tools/SecurityChecklist.vue') },
{ slug: 'adblocktest', emoji: '🛡️', titleKey: 'adblocktest.Title', noteKey: 'advancedtools.AdblockTest', category: 'network', component: () => import('@/components/advanced-tools/AdblockTest.vue') },
{ slug: 'whois', emoji: '📓', titleKey: 'whois.Title', noteKey: 'advancedtools.Whois', category: 'lookup', component: () => import('@/components/advanced-tools/Whois.vue') },
{ slug: 'asn', emoji: '🛂', titleKey: 'asnprofile.Title', noteKey: 'advancedtools.AsnProfile', category: 'lookup', component: () => import('@/components/advanced-tools/AsnProfile.vue'), requiresConfig: 'cloudFlare' },
{ slug: 'macchecker', emoji: '🗄️', titleKey: 'macchecker.Title', noteKey: 'advancedtools.MacChecker', category: 'lookup', component: () => import('@/components/advanced-tools/MacChecker.vue') },
+86
View File
@@ -984,6 +984,89 @@
}
}
},
"adblocktest": {
"Title": "Ad & Tracker Blocking Test",
"Note": "Built for anyone who runs an ad or tracker blocker: an extension such as uBlock Origin or Privacy Badger, a browser with built-in blocking, or filtering on a DNS service, VPN or router. The test has your browser contact well-known ad, analytics, social and device-maker endpoints, fetches popular tracking scripts without running them, and places bait elements that cosmetic filters hide, then shows what got through.",
"Note2": "A higher score is not automatically better. Not every tracker works against you: error reporting helps sites fix crashes, analytics helps them improve, and ads pay for much of the free web; some device-maker endpoints also deliver updates or notifications. Read the result as a map of what your setup lets through, and decide for yourself what is worth blocking.",
"Note3": "Requests that are not blocked do reach those services, without cookies or a referrer. Results stay in your browser and are not sent to us. An endpoint your network cannot reach for any other reason also shows as blocked.",
"Selected": "Selected checks:",
"SelectAll": "Select all",
"Invert": "Invert",
"Run": "Start test",
"Hint": "Choose what to test on the left, then press Start test.",
"Score": "Blocked share",
"Progress": "{done} / {total} checked",
"GroupCount": "{blocked} / {total} blocked",
"NothingOpen": "No request was confirmed as not blocked.",
"NothingBlocked": "No request was confirmed as blocked.",
"Version": "Test list version: {version}",
"Limitation": "These probes are plain requests sent by this page, not the way a website actually loads ads or scripts: filter rules aimed only at a specific request type (such as scripts or images) may let a probe through yet still block the real thing on a website.",
"count": {
"blocked": "Blocked",
"open": "Not blocked",
"unknown": "No answer"
},
"filter": {
"all": "All",
"open": "Not blocked only",
"blocked": "Blocked only"
},
"status": {
"blocked": "Blocked",
"open": "Not blocked",
"unknown": "No answer",
"pending": "Waiting"
},
"cosmeticStatus": {
"blocked": "Hidden",
"open": "Visible",
"unknown": "No answer",
"pending": "Waiting"
},
"cosmetic": {
"ad-container": "Ad container",
"adsense-slot": "AdSense slot"
},
"group": {
"ads": {
"title": "Ad networks",
"desc": "Ad servers and ad exchanges, checked by domain."
},
"analytics": {
"title": "Analytics & session recording",
"desc": "Visitor statistics, heatmap and session replay services."
},
"social": {
"title": "Social network trackers",
"desc": "Pixels and conversion trackers run by social platforms."
},
"errors": {
"title": "Error & performance monitoring",
"desc": "Crash reporting and real-user monitoring services."
},
"oem": {
"title": "Device maker telemetry",
"desc": "Telemetry and ad endpoints of phone, TV and device makers. Your browser never calls them on its own, so mainly DNS, VPN and router filters list them."
},
"scripts": {
"title": "Tracking scripts",
"desc": "Exact URLs of popular tag and pixel scripts, fetched but never run. Filters that match by path can catch these even where the domain itself is allowed."
},
"cosmetic": {
"title": "Element hiding",
"desc": "Bait elements shaped like ad slots. Only a blocker inside the browser can hide them; DNS, VPN and router filters never do, so this part is left out of the score."
}
},
"layer": {
"browser": "Bait elements were hidden: a blocker is working inside your browser, either an extension or the browser's own.",
"network": "Requests were blocked but the bait elements were not hidden: the blocking most likely happens outside the page, at a DNS service, VPN or router, or in the browser's tracking protection.",
"none": "Nothing was blocked: no ad or tracker blocking was found in this browser or on this network."
},
"report": {
"Category": "Category",
"Total": "Total"
}
},
"advancedtools": {
"Title": "Advanced Tools",
"Note": "People who want the very best from their network usually reach for extra tools to fine-tune their connection.",
@@ -1005,6 +1088,7 @@
"MacChecker": "Query information of a physical address",
"IpCalculator": "Subnet math, notation conversion and IPv6 insight for any IP, prefix or range",
"BrowserInfo": "Check browser information and fingerprint",
"AdblockTest": "Check what your ad blocker, DNS filter or VPN actually blocks",
"SecurityChecklist": "Guide to securing your digital life",
"PersonaCheck": "Compare what sites see against the country you want to look local to",
"IpBlocklist": "See whether an IP is on spam, abuse, proxy or VPN blocklists"
@@ -1451,6 +1535,7 @@
"mtrtest": "Per-hop MTR traceroutes toward the user's IP from probes around the world; loss at intermediate hops can be mere ICMP rate-limiting, sustained loss from one hop onward indicates a real problem.",
"ruletest": "Egress IPs observed when fetching 8 test endpoints — more than one unique IP means traffic is split across different routes (e.g. proxy rules).",
"browserinfo": "Browser, OS and display environment of the tested device.",
"adblocktest": "Ad & tracker blocking check run in the browser: per category (ad networks, analytics, social trackers, error monitoring, device-maker telemetry, tracking scripts), how many requests to well-known endpoints were blocked, got through, or timed out (noAnswer). Not blocking everything can be a deliberate choice, not a fault.",
"invisibility": "Proxy/VPN detection scores (0-100, higher = more likely detected) with the individual detection signals that flagged.",
"enhanceddnsleak": "Resolver-level DNS capture: every resolver IP that queried the test domain, with transport, ECS exposure and DNSSEC flags (do/cd).",
"persona": "How closely the browser matches a local resident of the chosen country: an overall grade plus each check's verdict. Verdicts only — the underlying values are deliberately not stored in a shared report."
@@ -1973,6 +2058,7 @@
"MacChecker": "Open MAC lookup panel",
"IpCalculator": "Open IP Calculator panel",
"BrowserInfo": "Open Browser Info panel",
"AdblockTest": "Open Ad & Tracker Blocking Test panel",
"SecurityChecklist": "Open Security Checklist panel",
"PersonaCheck": "Open In-depth Persona Check panel",
"IpBlocklist": "Open IP Blocklist Check panel",
+86
View File
@@ -984,6 +984,89 @@
}
}
},
"adblocktest": {
"Title": "Test de blocage des pubs et traceurs",
"Note": "Pour toute personne qui utilise un bloqueur de publicités ou de traceurs : une extension comme uBlock Origin ou Privacy Badger, un navigateur avec blocage intégré, ou le filtrage d'un service DNS, d'un VPN ou d'un routeur. Le test demande à votre navigateur de contacter des adresses connues de régies publicitaires, d'outils de statistiques, de réseaux sociaux et de fabricants d'appareils, récupère des scripts de pistage courants sans les exécuter et place des éléments leurres que les filtres cosmétiques masquent, puis indique ce qui est passé.",
"Note2": "Un score plus élevé n'est pas forcément meilleur. Tous les traceurs ne jouent pas contre vous : le signalement d'erreurs aide les sites à corriger les plantages, les statistiques les aident à s'améliorer, et la publicité finance une grande partie du web gratuit ; certaines adresses des fabricants servent aussi aux mises à jour ou aux notifications. Lisez le résultat comme une carte de ce que votre configuration laisse passer, et décidez vous-même de ce qui mérite d'être bloqué.",
"Note3": "Les requêtes non bloquées atteignent bien ces services, sans cookies ni référent. Les résultats restent dans votre navigateur et ne nous sont pas envoyés. Une adresse injoignable depuis votre réseau pour une autre raison apparaît aussi comme bloquée.",
"Selected": "Tests sélectionnés :",
"SelectAll": "Tout sélectionner",
"Invert": "Inverser",
"Run": "Lancer le test",
"Hint": "Choisissez à gauche ce qu'il faut tester, puis cliquez sur Lancer le test.",
"Score": "Part bloquée",
"Progress": "{done} / {total} vérifiés",
"GroupCount": "{blocked} / {total} bloqués",
"NothingOpen": "Aucune requête n'a été confirmée comme non bloquée.",
"NothingBlocked": "Aucune requête n'a été confirmée comme bloquée.",
"Version": "Version de la liste de test : {version}",
"Limitation": "Ces sondes sont de simples requêtes envoyées par cette page, et non la façon dont un site charge réellement publicités ou scripts : une règle de filtrage visant un type de requête précis (scripts, images…) peut laisser passer une sonde tout en bloquant le vrai contenu sur un site.",
"count": {
"blocked": "Bloqués",
"open": "Non bloqués",
"unknown": "Sans réponse"
},
"filter": {
"all": "Tout",
"open": "Non bloqués uniquement",
"blocked": "Bloqués uniquement"
},
"status": {
"blocked": "Bloqué",
"open": "Non bloqué",
"unknown": "Sans réponse",
"pending": "En attente"
},
"cosmeticStatus": {
"blocked": "Masqué",
"open": "Visible",
"unknown": "Sans réponse",
"pending": "En attente"
},
"cosmetic": {
"ad-container": "Conteneur publicitaire",
"adsense-slot": "Emplacement AdSense"
},
"group": {
"ads": {
"title": "Régies publicitaires",
"desc": "Serveurs et plateformes d'échange publicitaires, testés par domaine."
},
"analytics": {
"title": "Statistiques et enregistrement de sessions",
"desc": "Services de statistiques de visite, de cartes de chaleur et de relecture de sessions."
},
"social": {
"title": "Traceurs des réseaux sociaux",
"desc": "Pixels et traceurs de conversion des plateformes sociales."
},
"errors": {
"title": "Suivi des erreurs et des performances",
"desc": "Services de signalement de plantages et de mesure auprès des utilisateurs réels."
},
"oem": {
"title": "Télémétrie des fabricants d'appareils",
"desc": "Adresses de télémétrie et de publicité des fabricants de téléphones, téléviseurs et autres appareils. Votre navigateur ne les contacte jamais de lui-même ; ce sont surtout les filtres DNS, VPN et routeur qui les répertorient."
},
"scripts": {
"title": "Scripts de pistage",
"desc": "URL exactes de scripts de balises et de pixels courants, téléchargés mais jamais exécutés. Les filtres qui agissent sur le chemin peuvent les bloquer même quand le domaine est autorisé."
},
"cosmetic": {
"title": "Masquage d'éléments",
"desc": "Éléments leurres en forme d'emplacements publicitaires. Seul un bloqueur intégré au navigateur peut les masquer ; les filtres DNS, VPN et routeur n'y parviennent pas, cette partie est donc exclue du score."
}
},
"layer": {
"browser": "Les éléments leurres ont été masqués : un bloqueur agit dans votre navigateur, qu'il s'agisse d'une extension ou d'une fonction intégrée.",
"network": "Des requêtes ont été bloquées mais les éléments leurres n'ont pas été masqués : le blocage a très probablement lieu hors de la page, au niveau d'un service DNS, d'un VPN, d'un routeur ou de la protection contre le pistage du navigateur.",
"none": "Rien n'a été bloqué : aucun blocage de publicités ou de traceurs n'a été détecté dans ce navigateur ni sur ce réseau."
},
"report": {
"Category": "Catégorie",
"Total": "Total"
}
},
"advancedtools": {
"Title": "Outils avancés",
"Note": "Ceux qui exigent le meilleur de leur réseau utilisent souvent davantage d'outils pour optimiser leur connexion.",
@@ -1005,6 +1088,7 @@
"MacChecker": "Requête d'informations d'une adresse physique",
"IpCalculator": "Calcul de sous-réseau, conversion de notation et analyse IPv6 pour toute IP, préfixe ou plage",
"BrowserInfo": "Vérifier les informations du navigateur et l'empreinte digitale",
"AdblockTest": "Vérifiez ce que votre bloqueur de pubs, filtre DNS ou VPN bloque vraiment",
"SecurityChecklist": "Guide pour sécuriser votre vie numérique",
"PersonaCheck": "Comparez ce que voient les sites avec le pays dont vous voulez avoir l'air d'être un habitant",
"IpBlocklist": "Vérifiez si une IP figure sur des listes noires de spam, d’abus, de proxy ou de VPN"
@@ -1451,6 +1535,7 @@
"mtrtest": "Traceroutes MTR saut par saut vers l'IP de l'utilisateur depuis des sondes mondiales ; la perte sur des sauts intermédiaires peut n'être que du rate-limiting ICMP, une perte continue à partir d'un saut indique un vrai problème.",
"ruletest": "IP de sortie observées en accédant à 8 points de test — plusieurs IP distinctes signifient que le trafic est réparti sur différentes routes (règles de proxy, par ex.).",
"browserinfo": "Navigateur, système et environnement d'affichage de l'appareil testé.",
"adblocktest": "Test de blocage des publicités et traceurs exécuté dans le navigateur : par catégorie (régies publicitaires, statistiques, traceurs sociaux, suivi des erreurs, télémétrie des fabricants d'appareils, scripts de pistage), le nombre de requêtes vers des adresses connues qui ont été bloquées, sont passées ou ont expiré (noAnswer). Ne pas tout bloquer peut être un choix délibéré, pas une panne.",
"invisibility": "Scores de détection proxy/VPN (0-100, plus haut = plus probablement détecté) avec les signaux de détection déclenchés.",
"enhanceddnsleak": "Capture DNS au niveau résolveur : chaque IP de résolveur ayant interrogé le domaine de test, avec transport, exposition ECS et indicateurs DNSSEC (do/cd).",
"persona": "À quel point le navigateur correspond à un habitant du pays choisi : une note globale et le verdict de chaque vérification. Verdicts uniquement — les valeurs sous-jacentes ne sont volontairement pas stockées dans un rapport partagé."
@@ -1973,6 +2058,7 @@
"MacChecker": "Ouvrir le Recherche de MAC",
"IpCalculator": "Ouvrir le Calculateur IP",
"BrowserInfo": "Ouvrir l'Info du navigateur",
"AdblockTest": "Ouvrir le Test de blocage des pubs et traceurs",
"SecurityChecklist": "Ouvrir la Liste de sécurité",
"PersonaCheck": "Ouvrir le panneau Vérification de persona approfondie",
"IpBlocklist": "Ouvrir la Vérification des listes noires IP",
+86
View File
@@ -984,6 +984,89 @@
}
}
},
"adblocktest": {
"Title": "Teste de bloqueio de anúncios e rastreadores",
"Note": "Feito para quem usa bloqueio de anúncios ou rastreadores: uma extensão como uBlock Origin ou Privacy Badger, um navegador com bloqueio integrado, ou filtragem em um serviço de DNS, VPN ou roteador. O teste faz seu navegador acessar endereços conhecidos de redes de anúncios, análise, redes sociais e fabricantes de dispositivos, busca scripts de rastreamento populares sem executá-los e coloca elementos-isca que os filtros cosméticos ocultam; depois mostra o que passou.",
"Note2": "Uma pontuação maior não é automaticamente melhor. Nem todo rastreador age contra você: o relato de erros ajuda os sites a corrigir falhas, a análise os ajuda a melhorar e os anúncios bancam boa parte da web gratuita; alguns endereços de fabricantes também entregam atualizações ou notificações. Leia o resultado como um mapa do que sua configuração deixa passar e decida por conta própria o que vale a pena bloquear.",
"Note3": "As requisições não bloqueadas chegam de fato a esses serviços, sem cookies nem referenciador. Os resultados ficam no seu navegador e não são enviados para nós. Um endereço inacessível na sua rede por outro motivo também aparece como bloqueado.",
"Selected": "Testes selecionados:",
"SelectAll": "Selecionar tudo",
"Invert": "Inverter",
"Run": "Iniciar teste",
"Hint": "Escolha à esquerda o que testar e clique em Iniciar teste.",
"Score": "Parcela bloqueada",
"Progress": "{done} / {total} verificados",
"GroupCount": "{blocked} / {total} bloqueados",
"NothingOpen": "Nenhuma requisição foi confirmada como não bloqueada.",
"NothingBlocked": "Nenhuma requisição foi confirmada como bloqueada.",
"Version": "Versão da lista de teste: {version}",
"Limitation": "Estas sondagens são requisições simples enviadas por esta página, não a forma como um site realmente carrega anúncios ou scripts: regras de filtro voltadas só a um tipo de requisição (como scripts ou imagens) podem deixar uma sondagem passar e ainda assim bloquear o conteúdo real em um site.",
"count": {
"blocked": "Bloqueados",
"open": "Não bloqueados",
"unknown": "Sem resposta"
},
"filter": {
"all": "Todos",
"open": "Só não bloqueados",
"blocked": "Só bloqueados"
},
"status": {
"blocked": "Bloqueado",
"open": "Não bloqueado",
"unknown": "Sem resposta",
"pending": "Aguardando"
},
"cosmeticStatus": {
"blocked": "Oculto",
"open": "Visível",
"unknown": "Sem resposta",
"pending": "Aguardando"
},
"cosmetic": {
"ad-container": "Contêiner de anúncio",
"adsense-slot": "Espaço do AdSense"
},
"group": {
"ads": {
"title": "Redes de anúncios",
"desc": "Servidores e plataformas de troca de anúncios, verificados por domínio."
},
"analytics": {
"title": "Análise e gravação de sessões",
"desc": "Serviços de estatísticas de visitas, mapas de calor e reprodução de sessões."
},
"social": {
"title": "Rastreadores de redes sociais",
"desc": "Pixels e rastreadores de conversão das plataformas sociais."
},
"errors": {
"title": "Monitoramento de erros e desempenho",
"desc": "Serviços de relato de falhas e monitoramento de usuários reais."
},
"oem": {
"title": "Telemetria de fabricantes de dispositivos",
"desc": "Endereços de telemetria e anúncios de fabricantes de celulares, TVs e outros dispositivos. O navegador nunca os acessa por conta própria, então eles aparecem principalmente nas listas de filtros de DNS, VPN e roteador."
},
"scripts": {
"title": "Scripts de rastreamento",
"desc": "URLs exatas de scripts populares de tags e pixels, buscados mas nunca executados. Filtros que agem pelo caminho podem bloqueá-los mesmo quando o domínio é permitido."
},
"cosmetic": {
"title": "Ocultação de elementos",
"desc": "Elementos-isca com cara de espaço de anúncio. Só um bloqueador dentro do navegador consegue ocultá-los; filtros de DNS, VPN e roteador não fazem isso, então esta parte fica fora da pontuação."
}
},
"layer": {
"browser": "Os elementos-isca foram ocultados: há um bloqueador funcionando dentro do seu navegador, seja uma extensão ou o próprio navegador.",
"network": "Requisições foram bloqueadas, mas os elementos-isca não foram ocultados: o bloqueio provavelmente acontece fora da página, em um serviço de DNS, VPN, roteador ou na proteção contra rastreamento do navegador.",
"none": "Nada foi bloqueado: nenhum bloqueio de anúncios ou rastreadores foi encontrado neste navegador ou nesta rede."
},
"report": {
"Category": "Categoria",
"Total": "Total"
}
},
"advancedtools": {
"Title": "Ferramentas avançadas",
"Note": "Quem exige o máximo da qualidade da rede costuma usar mais ferramentas para otimizar a própria conexão.",
@@ -1005,6 +1088,7 @@
"MacChecker": "Consultar informações de um endereço físico",
"IpCalculator": "Cálculo de sub-rede, conversão de notação e análise IPv6 para qualquer IP, prefixo ou intervalo",
"BrowserInfo": "Verificar informações e impressão digital do navegador",
"AdblockTest": "Veja o que seu bloqueador de anúncios, filtro de DNS ou VPN realmente bloqueia",
"SecurityChecklist": "Guia para proteger sua vida digital",
"PersonaCheck": "Compare o que os sites veem com o país no qual você quer parecer local",
"IpBlocklist": "Veja se um IP está em listas negras de spam, abuso, proxy ou VPN"
@@ -1451,6 +1535,7 @@
"mtrtest": "Traceroutes MTR por salto até o IP do usuário a partir de sondas pelo mundo; perda em saltos intermediários pode ser apenas limitação de ICMP, mas perda sustentada a partir de um salto indica problema real.",
"ruletest": "IPs de saída observados ao buscar 8 endpoints de teste — mais de um IP único significa que o tráfego está dividido entre rotas diferentes (ex.: regras de proxy).",
"browserinfo": "Ambiente de navegador, sistema operacional e tela do dispositivo testado.",
"adblocktest": "Teste de bloqueio de anúncios e rastreadores executado no navegador: por categoria (redes de anúncios, análise, rastreadores sociais, monitoramento de erros, telemetria de fabricantes de dispositivos, scripts de rastreamento), quantas requisições a endereços conhecidos foram bloqueadas, passaram ou esgotaram o tempo (noAnswer). Não bloquear tudo pode ser uma escolha deliberada, não uma falha.",
"invisibility": "Pontuações de detecção de proxy/VPN (0–100, maior = mais provável de ser detectado) com os sinais individuais de detecção que foram sinalizados.",
"enhanceddnsleak": "Captura DNS em nível de resolvedor: cada IP de resolvedor que consultou o domínio de teste, com transporte, exposição ECS e flags DNSSEC (do/cd).",
"persona": "O quão próximo o navegador está de uma pessoa local do país escolhido: uma nota geral mais o veredito de cada verificação. Apenas vereditos — os valores subjacentes deliberadamente não são armazenados em relatórios compartilhados."
@@ -1973,6 +2058,7 @@
"MacChecker": "Abrir painel de consulta MAC",
"IpCalculator": "Abrir painel da Calculadora de IP",
"BrowserInfo": "Abrir painel de informações do navegador",
"AdblockTest": "Abrir painel do Teste de bloqueio de anúncios e rastreadores",
"SecurityChecklist": "Abrir painel de checklist de segurança",
"PersonaCheck": "Abrir painel de verificação aprofundada de persona",
"IpBlocklist": "Abrir painel de verificação de listas negras de IP",
+86
View File
@@ -984,6 +984,89 @@
}
}
},
"adblocktest": {
"Title": "Проверка блокировки рекламы и трекеров",
"Note": "Для тех, кто пользуется блокировкой рекламы или трекеров: расширением вроде uBlock Origin или Privacy Badger, браузером со встроенной блокировкой или фильтрацией на стороне DNS-сервиса, VPN или роутера. Тест обращается из вашего браузера к известным адресам рекламных сетей, систем аналитики, соцсетей и производителей устройств, загружает популярные скрипты отслеживания, не выполняя их, и размещает на странице элементы-приманки, которые скрывают косметические фильтры, а затем показывает, что прошло.",
"Note2": "Более высокий процент — не всегда лучше. Не каждый трекер работает против вас: сбор ошибок помогает сайтам исправлять сбои, аналитика — становиться лучше, а реклама оплачивает значительную часть бесплатного интернета; некоторые адреса производителей устройств также отвечают за обновления и уведомления. Воспринимайте результат как карту того, что пропускает ваша конфигурация, и сами решайте, что стоит блокировать.",
"Note3": "Незаблокированные запросы действительно доходят до этих сервисов, но без cookie и без реферера. Результаты остаются в вашем браузере и не отправляются нам. Адрес, недоступный из вашей сети по другой причине, тоже отображается как заблокированный.",
"Selected": "Выбрано проверок:",
"SelectAll": "Выбрать все",
"Invert": "Инвертировать",
"Run": "Начать проверку",
"Hint": "Выберите слева, что проверить, и нажмите «Начать проверку».",
"Score": "Доля заблокированных",
"Progress": "Проверено {done} из {total}",
"GroupCount": "Заблокировано {blocked} из {total}",
"NothingOpen": "Ни один запрос не подтверждён как незаблокированный.",
"NothingBlocked": "Ни один запрос не подтверждён как заблокированный.",
"Version": "Версия списка проверок: {version}",
"Limitation": "Эти проверки — обычные запросы с этой страницы, а не то, как сайт на самом деле загружает рекламу или скрипты: правила фильтрации, рассчитанные только на определённый тип запросов (например, скрипты или изображения), могут пропустить проверку, но всё равно заблокировать настоящий контент на сайте.",
"count": {
"blocked": "Заблокировано",
"open": "Не заблокировано",
"unknown": "Нет ответа"
},
"filter": {
"all": "Все",
"open": "Только незаблокированные",
"blocked": "Только заблокированные"
},
"status": {
"blocked": "Заблокировано",
"open": "Не заблокировано",
"unknown": "Нет ответа",
"pending": "Ожидание"
},
"cosmeticStatus": {
"blocked": "Скрыт",
"open": "Виден",
"unknown": "Нет ответа",
"pending": "Ожидание"
},
"cosmetic": {
"ad-container": "Рекламный контейнер",
"adsense-slot": "Блок AdSense"
},
"group": {
"ads": {
"title": "Рекламные сети",
"desc": "Рекламные серверы и биржи, проверка по домену."
},
"analytics": {
"title": "Аналитика и запись сессий",
"desc": "Сервисы статистики посещений, тепловых карт и записи сессий."
},
"social": {
"title": "Трекеры соцсетей",
"desc": "Пиксели и трекеры конверсий социальных платформ."
},
"errors": {
"title": "Мониторинг ошибок и производительности",
"desc": "Сервисы отчётов о сбоях и мониторинга реальных пользователей."
},
"oem": {
"title": "Телеметрия производителей устройств",
"desc": "Адреса телеметрии и рекламы производителей телефонов, телевизоров и других устройств. Браузер сам к ним не обращается, поэтому их включают в основном в списки DNS-, VPN- и роутерных фильтров."
},
"scripts": {
"title": "Скрипты отслеживания",
"desc": "Точные адреса популярных скриптов тегов и пикселей: загружаются, но не выполняются. Правила с сопоставлением по пути могут блокировать их, даже когда сам домен разрешён."
},
"cosmetic": {
"title": "Скрытие элементов",
"desc": "Элементы-приманки в виде рекламных блоков. Скрыть их может только блокировщик внутри браузера; DNS-, VPN- и роутерные фильтры на это не способны, поэтому эта часть не учитывается в доле."
}
},
"layer": {
"browser": "Элементы-приманки скрыты: внутри браузера работает блокировщик — расширение или встроенная функция браузера.",
"network": "Запросы блокируются, но элементы-приманки не скрыты: скорее всего, блокировка происходит вне страницы — на DNS-сервисе, VPN, роутере или во встроенной защите браузера от отслеживания.",
"none": "Ничего не заблокировано: в этом браузере и в этой сети блокировка рекламы и трекеров не обнаружена."
},
"report": {
"Category": "Категория",
"Total": "Всего"
}
},
"advancedtools": {
"Title": "Расширенные инструменты",
"Note": "Те, кто требователен к качеству сети, обычно используют больше инструментов, чтобы оптимизировать своё подключение.",
@@ -1005,6 +1088,7 @@
"MacChecker": "Поиск сведений о физическом адресе",
"IpCalculator": "Расчёт подсетей, преобразование записей и анализ IPv6 для любого IP, префикса или диапазона",
"BrowserInfo": "Проверка сведений и отпечатка браузера",
"AdblockTest": "Узнайте, что на самом деле блокирует ваш блокировщик рекламы, DNS-фильтр или VPN",
"SecurityChecklist": "Руководство по защите цифровой жизни",
"PersonaCheck": "Сравните то, что видят сайты, со страной, местным жителем которой вы хотите выглядеть",
"IpBlocklist": "Проверьте, есть ли IP в чёрных списках спама, злоупотреблений, прокси или VPN"
@@ -1451,6 +1535,7 @@
"mtrtest": "Пошаговые трассировки MTR до IP пользователя с точек по всему миру. Потеря на промежуточных узлах может быть лишь ограничением частоты ICMP; постоянная потеря, продолжающаяся от одного узла дальше, указывает на реальную проблему.",
"ruletest": "Выходные IP, обнаруженные при обращении к 8 тестовым адресам. Несколько уникальных IP означают, что трафик разделён между маршрутами, например правилами прокси.",
"browserinfo": "Браузер, ОС и параметры экрана проверяемого устройства.",
"adblocktest": "Проверка блокировки рекламы и трекеров в браузере: по категориям (рекламные сети, аналитика, трекеры соцсетей, мониторинг ошибок, телеметрия производителей устройств, скрипты отслеживания) — сколько запросов к известным адресам было заблокировано, прошло или завершилось тайм-аутом (noAnswer). Неполная блокировка может быть осознанным выбором, а не неисправностью.",
"invisibility": "Оценки обнаружения прокси/VPN (0–100; чем выше, тем вероятнее обнаружение) и отдельные сработавшие признаки.",
"enhanceddnsleak": "Перехват DNS на уровне резолверов: каждый IP резолвера, запросившего тестовый домен, с данными о транспорте, раскрытии ECS и флагах DNSSEC (do/cd).",
"persona": "Насколько браузер похож на местного жителя выбранной страны: общая оценка и вердикт каждой проверки. Только вердикты — стоящие за ними значения намеренно не сохраняются в общем отчёте."
@@ -1973,6 +2058,7 @@
"MacChecker": "Открыть панель поиска MAC-адреса",
"IpCalculator": "Открыть панель IP-калькулятора",
"BrowserInfo": "Открыть панель сведений о браузере",
"AdblockTest": "Открыть панель проверки блокировки рекламы и трекеров",
"SecurityChecklist": "Открыть панель контрольного списка безопасности",
"PersonaCheck": "Открыть панель углублённой проверки портрета",
"IpBlocklist": "Открыть панель проверки IP по чёрным спискам",
+86
View File
@@ -984,6 +984,89 @@
}
}
},
"adblocktest": {
"Title": "廣告與追蹤封鎖檢測",
"Note": "適用於正在使用廣告或追蹤封鎖的人:uBlock Origin、隱私獾(Privacy Badger)等瀏覽器擴充功能,內建阻擋功能的瀏覽器,或 DNS 服務、VPN、路由器上的過濾功能。測試會讓瀏覽器連線到常見的廣告、統計、社群平台與裝置廠商的服務端點,請求常見的追蹤腳本(只請求不執行),並在頁面中放置會被元素隱藏規則命中的誘餌元素,然後顯示哪些被放行了。",
"Note2": "阻擋比例越高不一定越好。並非所有追蹤都在損害你:錯誤監控幫助網站修復當機,統計分析幫助網站改進,廣告支撐著大量免費內容;部分裝置廠商的端點也負責系統更新或訊息推播。請把結果當作一份「你的設定放行了什麼」的地圖,再自行決定哪些值得封鎖。",
"Note3": "沒有被阻擋的請求會真實到達這些服務,但不會攜帶 Cookie 或來源頁面資訊。檢測結果只保留在你的瀏覽器中,不會傳送給我們。如果某個端點因為其他原因在你的網路中無法連線,也會顯示為已阻擋。",
"Selected": "已選檢測項目:",
"SelectAll": "全選",
"Invert": "反選",
"Run": "開始檢測",
"Hint": "在左側選擇要檢測的項目,然後點選「開始檢測」。",
"Score": "阻擋比例",
"Progress": "已檢測 {done} / {total}",
"GroupCount": "已阻擋 {blocked} / {total}",
"NothingOpen": "沒有確認未被阻擋的請求。",
"NothingBlocked": "沒有確認被阻擋的請求。",
"Version": "檢測清單版本:{version}",
"Limitation": "這些檢測是由本頁面直接送出的一般請求,並不是網站真正載入廣告或腳本的方式:只針對特定請求類型(例如腳本或圖片)的過濾規則,可能放行這裡的檢測請求,卻仍會在真實網站上阻擋對應內容。",
"count": {
"blocked": "已阻擋",
"open": "未阻擋",
"unknown": "無回應"
},
"filter": {
"all": "全部",
"open": "只看未阻擋",
"blocked": "只看已阻擋"
},
"status": {
"blocked": "已阻擋",
"open": "未阻擋",
"unknown": "無回應",
"pending": "等待中"
},
"cosmeticStatus": {
"blocked": "已隱藏",
"open": "可見",
"unknown": "無回應",
"pending": "等待中"
},
"cosmetic": {
"ad-container": "廣告容器",
"adsense-slot": "AdSense 廣告欄位"
},
"group": {
"ads": {
"title": "廣告網路",
"desc": "廣告伺服器與廣告交易平台,依網域檢測。"
},
"analytics": {
"title": "統計分析與工作階段錄製",
"desc": "訪客統計、熱點圖與工作階段重播服務。"
},
"social": {
"title": "社群平台追蹤",
"desc": "社群平台的像素與轉換追蹤。"
},
"errors": {
"title": "錯誤與效能監控",
"desc": "當機回報與真實使用者監控服務。"
},
"oem": {
"title": "裝置廠商遙測",
"desc": "手機、電視等裝置廠商的遙測與廣告端點。瀏覽器本身不會主動連線到它們,主要由 DNS、VPN 和路由器的過濾清單收錄。"
},
"scripts": {
"title": "追蹤腳本",
"desc": "常見統計代碼與像素腳本的完整網址,只請求不執行。依路徑比對的規則即使在網域放行時也可能擋下它們。"
},
"cosmetic": {
"title": "元素隱藏",
"desc": "外觀類似廣告欄位的誘餌元素。只有在瀏覽器內執行的阻擋器才能隱藏它們,DNS、VPN 和路由器的過濾做不到,所以這一部分不計入阻擋比例。"
}
},
"layer": {
"browser": "誘餌元素被隱藏了:瀏覽器內有阻擋器在運作,可能是擴充功能,也可能是瀏覽器內建的功能。",
"network": "請求被阻擋,但誘餌元素沒有被隱藏:阻擋很可能發生在頁面之外,例如 DNS 服務、VPN、路由器,或瀏覽器的防追蹤功能。",
"none": "沒有任何阻擋:在這個瀏覽器和網路中沒有發現廣告或追蹤封鎖。"
},
"report": {
"Category": "類別",
"Total": "總數"
}
},
"advancedtools": {
"Title": "進階工具",
"Note": "對網路品質追求極致的人,往往會使用更多工具來最佳化自己的網路連線。",
@@ -1005,6 +1088,7 @@
"MacChecker": "查詢實體位址的歸屬資訊",
"IpCalculator": "任意 IP、前綴或範圍的子網路計算、進位轉換與 IPv6 解讀",
"BrowserInfo": "檢閱瀏覽器資訊和指紋",
"AdblockTest": "檢查你的廣告阻擋器、DNS 過濾或 VPN 實際擋下了什麼",
"SecurityChecklist": "全面的數位生活安全檢查清單",
"PersonaCheck": "把網站看到的你,和你想扮成的本地人做對照",
"IpBlocklist": "查看 IP 是否出現在垃圾郵件、濫用、代理或 VPN 黑名單上"
@@ -1451,6 +1535,7 @@
"mtrtest": "全球探針針對使用者 IP 進行 MTR 路由追蹤。途中某些節點發生封包遺失,可能只是 ICMP 速率限制;如果從某個節點開始,後續都持續出現封包遺失,才較可能代表實際問題。",
"ruletest": "存取 8 個測試端點時觀測到的出口 IP——出現多個不同 IP 表示流量被分流到不同線路(如代理規則)。",
"browserinfo": "被測裝置的瀏覽器、作業系統與顯示環境。",
"adblocktest": "在瀏覽器中執行的廣告與追蹤封鎖檢測:依類別(廣告網路、統計分析、社群平台追蹤、錯誤監控、裝置廠商遙測、追蹤腳本)統計送往常見端點的請求中,被阻擋、未被阻擋和逾時(noAnswer)的數量。沒有全部阻擋可能是使用者的主動選擇,並不代表故障。",
"invisibility": "代理/VPN 檢測得分(0-100,越高越可能被識別)及各項命中的檢測訊號。",
"enhanceddnsleak": "解析器層級的 DNS 查詢紀錄:包含向測試網域名稱發出查詢的每個解析器 IP、傳輸協定、ECS 暴露情況與 DNSSEC 標記(DO/CD)。",
"persona": "瀏覽器與所選國家本地居民的相符程度:一個總體評級,加上每項檢查的結論。只有結論——各項檢查依據的具體數值刻意不寫入可分享的報告。"
@@ -1973,6 +2058,7 @@
"MacChecker": "開啟實體位址查詢面板",
"IpCalculator": "開啟 IP 計算器面板",
"BrowserInfo": "開啟瀏覽器資訊面板",
"AdblockTest": "開啟廣告與追蹤封鎖檢測面板",
"SecurityChecklist": "開啟安全檢查清單",
"PersonaCheck": "開啟深度畫像檢測面板",
"IpBlocklist": "開啟 IP 黑名單檢查面板",
+86
View File
@@ -984,6 +984,89 @@
}
}
},
"adblocktest": {
"Title": "广告与跟踪屏蔽检测",
"Note": "适用于正在使用广告或跟踪屏蔽的人:uBlock Origin、隐私獾(Privacy Badger)等浏览器扩展,自带拦截功能的浏览器,或 DNS 服务、VPN、路由器上的过滤功能。测试会让浏览器访问常见的广告、统计、社交平台和设备厂商的服务端点,请求常见的跟踪脚本(只请求不执行),并在页面中放置会被元素隐藏规则命中的诱饵元素,然后展示哪些被放行了。",
"Note2": "拦截比例越高不一定越好。并不是所有跟踪都在损害你:错误监控帮助网站修复崩溃,统计分析帮助网站改进,广告支撑着大量免费内容;部分设备厂商的端点也承担着系统更新或消息推送。请把结果当作一份「你的配置放行了什么」的地图,再自行决定哪些值得屏蔽。",
"Note3": "没有被拦截的请求会真实到达这些服务,但不会携带 Cookie 或来源页面信息。检测结果只保留在你的浏览器中,不会发送给我们。如果某个端点因为其他原因在你的网络中无法访问,也会显示为已拦截。",
"Selected": "已选检测项:",
"SelectAll": "全选",
"Invert": "反选",
"Run": "开始检测",
"Hint": "在左侧选择要检测的项目,然后点击「开始检测」。",
"Score": "拦截比例",
"Progress": "已检测 {done} / {total}",
"GroupCount": "已拦截 {blocked} / {total}",
"NothingOpen": "没有确认未被拦截的请求。",
"NothingBlocked": "没有确认被拦截的请求。",
"Version": "检测名单版本:{version}",
"Limitation": "这些检测是由本页面直接发出的普通请求,并不是网站真正加载广告或脚本的方式:只针对特定请求类型(例如脚本或图片)的过滤规则,可能放行这里的检测请求,却仍会在真实网站上拦截对应内容。",
"count": {
"blocked": "已拦截",
"open": "未拦截",
"unknown": "无响应"
},
"filter": {
"all": "全部",
"open": "仅看未拦截",
"blocked": "仅看已拦截"
},
"status": {
"blocked": "已拦截",
"open": "未拦截",
"unknown": "无响应",
"pending": "等待中"
},
"cosmeticStatus": {
"blocked": "已隐藏",
"open": "可见",
"unknown": "无响应",
"pending": "等待中"
},
"cosmetic": {
"ad-container": "广告容器",
"adsense-slot": "AdSense 广告位"
},
"group": {
"ads": {
"title": "广告网络",
"desc": "广告服务器与广告交易平台,按域名检测。"
},
"analytics": {
"title": "统计分析与会话录制",
"desc": "访客统计、热力图与会话回放服务。"
},
"social": {
"title": "社交平台跟踪",
"desc": "社交平台的像素与转化跟踪。"
},
"errors": {
"title": "错误与性能监控",
"desc": "崩溃上报与真实用户监控服务。"
},
"oem": {
"title": "设备厂商遥测",
"desc": "手机、电视等设备厂商的遥测与广告端点。浏览器本身不会主动访问它们,主要由 DNS、VPN 和路由器的过滤名单收录。"
},
"scripts": {
"title": "跟踪脚本",
"desc": "常见统计代码与像素脚本的完整地址,只请求不执行。按路径匹配的规则即使在域名放行时也可能拦住它们。"
},
"cosmetic": {
"title": "元素隐藏",
"desc": "外形类似广告位的诱饵元素。只有运行在浏览器内的拦截器才能隐藏它们,DNS、VPN 和路由器的过滤做不到,所以这一部分不计入拦截比例。"
}
},
"layer": {
"browser": "诱饵元素被隐藏了:浏览器内有拦截器在工作,可能是扩展,也可能是浏览器自带的功能。",
"network": "请求被拦截,但诱饵元素没有被隐藏:拦截很可能发生在页面之外,例如 DNS 服务、VPN、路由器,或浏览器的防跟踪功能。",
"none": "没有任何拦截:在这个浏览器和网络中没有发现广告或跟踪屏蔽。"
},
"report": {
"Category": "类别",
"Total": "总数"
}
},
"advancedtools": {
"Title": "高级工具",
"Note": "对网络质量追求极致的人,往往会使用更多工具来优化自己的网络连接。",
@@ -1005,6 +1088,7 @@
"MacChecker": "查询物理地址的归属信息",
"IpCalculator": "任意 IP、前缀或范围的子网计算、进制转换与 IPv6 解读",
"BrowserInfo": "检阅浏览器信息和指纹",
"AdblockTest": "检查你的广告拦截器、DNS 过滤或 VPN 实际拦住了什么",
"SecurityChecklist": "全面的数字生活安全检查清单",
"PersonaCheck": "把网站看到的你,和你想扮成的本地人做对照",
"IpBlocklist": "查看 IP 是否出现在垃圾邮件、滥用、代理或 VPN 黑名单上"
@@ -1451,6 +1535,7 @@
"mtrtest": "全球探针到用户 IP 的逐跳 MTR 路由追踪;中间跳的丢包可能只是 ICMP 限速,从某一跳起持续丢包才代表真实问题。",
"ruletest": "访问 8 个测试端点时观测到的出口 IP——出现多个不同 IP 说明流量被分流到不同线路(如代理规则)。",
"browserinfo": "被测设备的浏览器、操作系统与显示环境。",
"adblocktest": "在浏览器中运行的广告与跟踪屏蔽检测:按类别(广告网络、统计分析、社交平台跟踪、错误监控、设备厂商遥测、跟踪脚本)统计发往常见端点的请求中,被拦截、未被拦截和超时(noAnswer)的数量。没有全部拦截可能是用户的主动选择,并不代表故障。",
"invisibility": "代理/VPN 检测得分(0-100,越高越可能被识别)及各项命中的检测信号。",
"enhanceddnsleak": "解析器级 DNS 捕获:向测试域名发起查询的每个解析器 IP,含传输协议、ECS 暴露与 DNSSEC 标志(do/cd)。",
"persona": "浏览器与所选国家本地居民的相符程度:一个总体评级,加上每项检查的结论。只有结论——各项检查依据的具体数值刻意不写入可分享的报告。"
@@ -1973,6 +2058,7 @@
"MacChecker": "打开物理地址查询面板",
"IpCalculator": "打开 IP 计算器面板",
"BrowserInfo": "打开浏览器信息面板",
"AdblockTest": "打开广告与跟踪屏蔽检测面板",
"SecurityChecklist": "打开安全检查清单",
"PersonaCheck": "打开深度画像检测面板",
"IpBlocklist": "打开 IP 黑名单检查面板",
+138
View File
@@ -0,0 +1,138 @@
// Pure layer of the Ad & Tracker Blocking Test (AdblockTest.vue): turning the
// probe list (data/adblock-probes.js) into a run plan, classifying one probe
// request, tallying statuses, and the verdicts drawn from them. DOM work
// (cosmetic baits) stays in the component; only its predicate lives here.
import { fetchWithTimeout } from '../fetch-with-timeout.js';
// A per-target status: 'pending' before its answer, then one of
// blocked — the request never left (blocker / DNS sinkhole / refused)
// open — any response came back (opaque, status unknowable)
// unknown — timed out; a stalled request proves neither
export const PROBE_TIMEOUT_MS = 8000;
export const PROBE_CONCURRENCY = 24;
// Gecko on macOS stalls with many blocked requests in flight.
export const GECKO_MAC_TIMEOUT_MS = 5000;
export const GECKO_MAC_CONCURRENCY = 6;
// How long a cosmetic bait may take to be hidden, and how often to look.
export const COSMETIC_WAIT_MS = 2000;
export const COSMETIC_POLL_MS = 250;
// Bait elements, keyed by the target ids of the 'cosmetic' group: the class
// names and markup generic cosmetic filters (EasyList and the like) hide.
export const COSMETIC_BAITS = {
'ad-container': { tag: 'div', className: 'ad-banner adsbox ad-placement banner-ads textads', attrs: {} },
'adsense-slot': { tag: 'ins', className: 'adsbygoogle', attrs: { 'data-ad-client': 'ca-pub-0000000000000000', 'data-ad-slot': '0000000000' } },
};
export const vendorKey = (groupId, vendorId) => `${groupId}/${vendorId}`;
export const allVendorKeys = (groups) => groups.flatMap((g) => g.vendors.map((v) => vendorKey(g.id, v.id)));
// The URL a target is fetched at; cosmetic targets have none.
export const probeUrlOf = (kind, target) => {
if (kind === 'domain') return `https://${target}/`;
if (kind === 'script') return target;
return null;
};
// Flat run plan for the selected vendors, in list order:
// [{ id, groupId, vendorId, kind, target, url }].
export const buildPlan = (groups, selectedKeys) => {
const selected = new Set(selectedKeys);
return groups.flatMap((g) => g.vendors
.filter((v) => selected.has(vendorKey(g.id, v.id)))
.flatMap((v) => v.targets.map((target) => ({
id: `${vendorKey(g.id, v.id)}/${target}`,
groupId: g.id,
vendorId: v.id,
kind: g.kind,
target,
url: probeUrlOf(g.kind, target),
}))));
};
// One no-cors request → 'open' | 'blocked' | 'unknown'. No cookies, no
// referrer: an unblocked probe tells the third party nothing about us.
export const probeRequest = async (url, { timeoutMs = PROBE_TIMEOUT_MS } = {}) => {
try {
await fetchWithTimeout(url, {
method: 'HEAD',
mode: 'no-cors',
credentials: 'omit',
cache: 'no-store',
referrerPolicy: 'no-referrer',
timeoutMs,
});
return 'open';
} catch (error) {
return error?.name === 'AbortError' ? 'unknown' : 'blocked';
}
};
// Run async tasks with at most `limit` in flight; resolves when all settle.
export const runWithConcurrency = async (tasks, limit) => {
let next = 0;
const worker = async () => {
while (next < tasks.length) {
const task = tasks[next++];
await task();
}
};
await Promise.all(Array.from({ length: Math.min(limit, tasks.length) }, worker));
};
// Firefox-family on macOS (iOS browsers are WebKit and excluded).
export const isGeckoMac = (ua = '', platform = '') => {
if (/iPhone|iPad|iPod|FxiOS|CriOS|EdgiOS/.test(ua)) return false;
const gecko = /\b(Firefox|Waterfox|LibreWolf|IceCat)\//.test(ua);
const mac = /Mac/.test(platform) || /Macintosh/.test(ua);
return gecko && mac;
};
export const probeLimits = (ua, platform) => (isGeckoMac(ua, platform)
? { timeoutMs: GECKO_MAC_TIMEOUT_MS, concurrency: GECKO_MAC_CONCURRENCY }
: { timeoutMs: PROBE_TIMEOUT_MS, concurrency: PROBE_CONCURRENCY });
// A bait counts as hidden when it was removed, collapsed or display:none'd.
export const isBaitHidden = ({ connected, offsetHeight, display, visibility }) =>
!connected || offsetHeight === 0 || display === 'none' || visibility === 'hidden';
// Counts over a list of statuses. `score` is the blocked share of the
// answered ones (unknown excluded), 0–100, or null when none answered.
export const tally = (statuses) => {
const counts = { total: statuses.length, blocked: 0, open: 0, unknown: 0, pending: 0 };
for (const s of statuses) {
if (s in counts && s !== 'total') counts[s] += 1;
else counts.pending += 1;
}
const answered = counts.blocked + counts.open;
counts.score = answered ? Math.round((counts.blocked / answered) * 100) : null;
return counts;
};
// A probe status as a status tone (composables/use-status-tone.js): blocked
// reads as a stop (fail, red), not blocked as passing through (ok-fast,
// green), the rest as wait.
export const statusTone = (status) => ({ blocked: 'fail', open: 'ok-fast' })[status] ?? 'wait';
// Picker "invert": every key in `allKeys` not currently selected.
export const invertSelection = (allKeys, selected) => {
const current = new Set(selected);
return allKeys.filter((key) => !current.has(key));
};
// Where the blocking most likely happens, from what was tested:
// browser — a bait got hidden: an extension or a browser's own blocker
// network — requests blocked, baits untouched: DNS / VPN / router level
// none — nothing blocked anywhere
// null when the answer needs a part that wasn't run (or nothing answered).
export const blockingLayer = ({ requests, cosmetic }) => {
const hasRequests = requests.blocked + requests.open > 0;
const hasCosmetic = cosmetic.blocked + cosmetic.open > 0;
if (hasCosmetic && cosmetic.blocked > 0) return 'browser';
if (!hasRequests) return null;
if (requests.blocked === 0) return hasCosmetic ? 'none' : null;
return hasCosmetic ? 'network' : null;
};
+23
View File
@@ -283,6 +283,28 @@ const buildBrowserinfo = (payload) => {
return Object.keys(section).length ? section : null;
};
// adblocktest:finished — { items: [{ groupId, kind, status }] }, one item
// per probed address. Only the per-category request counts travel; cosmetic
// baits and unanswered (pending) items are dropped.
const ADBLOCK_STATUSES = new Set(['blocked', 'open', 'unknown']);
const buildAdblocktest = (payload) => {
const groups = new Map(); // first-appearance order = the probe list's
for (const item of Array.isArray(payload?.items) ? payload.items : []) {
if (typeof item?.groupId !== 'string' || item.kind === 'cosmetic'
|| !ADBLOCK_STATUSES.has(item?.status)) continue;
const id = clip(item.groupId, 16);
if (!groups.has(id)) {
if (groups.size >= 16) continue;
groups.set(id, { id, total: 0, blocked: 0, open: 0, unknown: 0 });
}
const counts = groups.get(id);
if (counts.total >= 1024) continue;
counts.total += 1;
counts[item.status] += 1;
}
return groups.size ? { groups: [...groups.values()] } : null;
};
// invisibility:result — { proxyScore, vpnScore, ip, flags: [{ key, flagged }] }
const buildInvisibility = (payload) => {
const proxy = finiteNum(payload?.proxyScore, 0, 100);
@@ -381,6 +403,7 @@ export const REPORT_EVENT_BUILDERS = {
'mtrtest:finished': { section: 'mtrtest', build: buildMtrtest },
'ruletest:finished': { section: 'ruletest', build: buildRuletest },
'browserinfo:finished': { section: 'browserinfo', build: buildBrowserinfo },
'adblocktest:finished': { section: 'adblocktest', build: buildAdblocktest },
'invisibility:result': { section: 'invisibility', build: buildInvisibility },
'enhanceddnsleak:finished': { section: 'enhanceddnsleak', build: buildEnhanceddnsleak },
'persona:finished': { section: 'persona', build: buildPersona },
+3
View File
@@ -19,6 +19,7 @@ export const SECTION_TITLE_KEYS = {
mtrtest: 'mtrtest.Title',
ruletest: 'ruletest.Title',
browserinfo: 'browserinfo.Title',
adblocktest: 'adblocktest.Title',
invisibility: 'invisibilitytest.Title',
enhanceddnsleak: 'enhanceddnsleaktest.Title',
persona: 'personacheck.Title',
@@ -157,6 +158,8 @@ const SECTION_RENDERERS = {
['display', section.display && `${section.display.width}x${section.display.height} @${section.display.pixelRatio}x`],
['connection', section.connection && `${section.connection.effectiveType ?? ''} downlink=${section.connection.downlink ?? '—'}Mbps rtt=${section.connection.rtt ?? '—'}ms`.trim()],
]),
adblocktest: (section) => mdTable(['group', 'total', 'blocked', 'notBlocked', 'noAnswer'],
section.groups.map((g) => [g.id, g.total, g.blocked, g.open, g.unknown])),
invisibility: (section) => [
kvLines([
['ip', section.ip],
+228
View File
@@ -0,0 +1,228 @@
// Tests for the Ad & Tracker Blocking Test: the shape of the static probe
// list (frontend/data/adblock-probes.js) and the pure layer in
// frontend/utils/features/adblock-test.js.
import assert from 'node:assert/strict';
import { afterEach, describe, it } from 'node:test';
import { ADBLOCK_PROBE_GROUPS, ADBLOCK_PROBES_VERSION } from '../frontend/data/adblock-probes.js';
import {
COSMETIC_BAITS,
allVendorKeys,
blockingLayer,
buildPlan,
isBaitHidden,
isGeckoMac,
probeLimits,
probeRequest,
probeUrlOf,
runWithConcurrency,
tally,
statusTone,
invertSelection,
vendorKey,
GECKO_MAC_CONCURRENCY,
PROBE_CONCURRENCY,
} from '../frontend/utils/features/adblock-test.js';
const realFetch = globalThis.fetch;
afterEach(() => { globalThis.fetch = realFetch; });
const HOST = /^(?=.{1,253}$)([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/;
describe('adblock probe list', () => {
it('carries a dated version', () => {
assert.match(ADBLOCK_PROBES_VERSION, /^\d{4}-\d{2}-\d{2}$/);
});
it('has unique group ids and unique vendor ids per group', () => {
const groupIds = ADBLOCK_PROBE_GROUPS.map((g) => g.id);
assert.equal(new Set(groupIds).size, groupIds.length);
for (const g of ADBLOCK_PROBE_GROUPS) {
const ids = g.vendors.map((v) => v.id);
assert.equal(new Set(ids).size, ids.length, `${g.id}: duplicate vendor id`);
assert.ok(g.vendors.length > 0, `${g.id}: no vendors`);
}
});
it('every vendor has targets of its group kind', () => {
for (const g of ADBLOCK_PROBE_GROUPS) {
assert.ok(['domain', 'script', 'cosmetic'].includes(g.kind), `${g.id}: kind ${g.kind}`);
for (const v of g.vendors) {
assert.ok(v.targets.length > 0, `${g.id}/${v.id}: no targets`);
if (g.kind !== 'cosmetic') assert.equal(typeof v.name, 'string', `${g.id}/${v.id}: name`);
for (const t of v.targets) {
if (g.kind === 'domain') assert.match(t, HOST, `${g.id}/${v.id}: ${t}`);
if (g.kind === 'script') {
const url = new URL(t);
assert.equal(url.protocol, 'https:', t);
assert.match(url.hostname, HOST, t);
}
if (g.kind === 'cosmetic') assert.ok(COSMETIC_BAITS[t], `${t}: no bait`);
}
}
}
});
it('lists no domain twice', () => {
const hosts = ADBLOCK_PROBE_GROUPS.filter((g) => g.kind === 'domain')
.flatMap((g) => g.vendors.flatMap((v) => v.targets));
const dupes = hosts.filter((h, i) => hosts.indexOf(h) !== i);
assert.deepEqual(dupes, []);
});
});
describe('buildPlan', () => {
const groups = [
{ id: 'ads', kind: 'domain', vendors: [{ id: 'a', targets: ['a.example.com', 'b.example.com'] }, { id: 'b', targets: ['c.example.com'] }] },
{ id: 'scripts', kind: 'script', vendors: [{ id: 's', targets: ['https://x.example.com/t.js'] }] },
];
it('expands only the selected vendors, in list order', () => {
const plan = buildPlan(groups, [vendorKey('scripts', 's'), vendorKey('ads', 'a')]);
assert.deepEqual(plan.map((p) => p.url), [
'https://a.example.com/', 'https://b.example.com/', 'https://x.example.com/t.js',
]);
assert.deepEqual(plan[0], {
id: 'ads/a/a.example.com', groupId: 'ads', vendorId: 'a', kind: 'domain',
target: 'a.example.com', url: 'https://a.example.com/',
});
});
it('allVendorKeys selects everything', () => {
assert.equal(buildPlan(groups, allVendorKeys(groups)).length, 4);
assert.deepEqual(buildPlan(groups, []), []);
});
it('probeUrlOf maps each kind', () => {
assert.equal(probeUrlOf('domain', 'x.com'), 'https://x.com/');
assert.equal(probeUrlOf('script', 'https://x.com/a.js'), 'https://x.com/a.js');
assert.equal(probeUrlOf('cosmetic', 'ad-container'), null);
});
});
describe('probeRequest', () => {
it('any response is open, and the request carries no credentials or referrer', async () => {
let init;
globalThis.fetch = async (_url, i) => { init = i; return { status: 0, type: 'opaque' }; };
assert.equal(await probeRequest('https://x.example.com/'), 'open');
assert.equal(init.mode, 'no-cors');
assert.equal(init.credentials, 'omit');
assert.equal(init.referrerPolicy, 'no-referrer');
});
it('a network error is blocked', async () => {
globalThis.fetch = async () => { throw new TypeError('Failed to fetch'); };
assert.equal(await probeRequest('https://x.example.com/'), 'blocked');
});
it('a timeout is unknown', async () => {
globalThis.fetch = (_url, { signal }) => new Promise((_, reject) => {
signal.addEventListener('abort', () => reject(Object.assign(new Error('aborted'), { name: 'AbortError' })));
});
assert.equal(await probeRequest('https://x.example.com/', { timeoutMs: 10 }), 'unknown');
});
});
describe('runWithConcurrency', () => {
it('runs every task with at most `limit` in flight', async () => {
let inFlight = 0;
let peak = 0;
const done = [];
const tasks = Array.from({ length: 10 }, (_, i) => async () => {
inFlight += 1;
peak = Math.max(peak, inFlight);
await new Promise((r) => setTimeout(r, 2));
inFlight -= 1;
done.push(i);
});
await runWithConcurrency(tasks, 3);
assert.equal(done.length, 10);
assert.equal(peak, 3);
});
it('handles an empty list', async () => {
await runWithConcurrency([], 4);
});
});
describe('isGeckoMac / probeLimits', () => {
const FF_MAC = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:140.0) Gecko/20100101 Firefox/140.0';
const FF_IOS = 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/140.0 Mobile/15E148 Safari/605.1.15';
const CHROME_MAC = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36';
const FF_WIN = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0';
it('matches only Firefox-family on macOS', () => {
assert.equal(isGeckoMac(FF_MAC, 'MacIntel'), true);
assert.equal(isGeckoMac(FF_IOS, 'iPhone'), false);
assert.equal(isGeckoMac(CHROME_MAC, 'MacIntel'), false);
assert.equal(isGeckoMac(FF_WIN, 'Win32'), false);
});
it('throttles Gecko on macOS', () => {
assert.equal(probeLimits(FF_MAC, 'MacIntel').concurrency, GECKO_MAC_CONCURRENCY);
assert.equal(probeLimits(CHROME_MAC, 'MacIntel').concurrency, PROBE_CONCURRENCY);
});
});
describe('isBaitHidden', () => {
const shown = { connected: true, offsetHeight: 10, display: 'block', visibility: 'visible' };
it('a laid-out, visible bait is shown', () => assert.equal(isBaitHidden(shown), false));
it('removed, collapsed, display:none or hidden all count as hidden', () => {
assert.equal(isBaitHidden({ ...shown, connected: false }), true);
assert.equal(isBaitHidden({ ...shown, offsetHeight: 0 }), true);
assert.equal(isBaitHidden({ ...shown, display: 'none' }), true);
assert.equal(isBaitHidden({ ...shown, visibility: 'hidden' }), true);
});
});
describe('tally', () => {
it('counts statuses and scores the answered ones', () => {
assert.deepEqual(tally(['blocked', 'blocked', 'open', 'unknown', 'pending']), {
total: 5, blocked: 2, open: 1, unknown: 1, pending: 1, score: 67,
});
});
it('scores null when nothing answered', () => {
assert.equal(tally(['unknown', 'pending']).score, null);
assert.equal(tally([]).score, null);
});
});
describe('statusTone / invertSelection', () => {
it('blocked is a stop, not blocked passes through, the rest wait', () => {
assert.equal(statusTone('blocked'), 'fail');
assert.equal(statusTone('open'), 'ok-fast');
assert.equal(statusTone('unknown'), 'wait');
assert.equal(statusTone('pending'), 'wait');
});
it('inverts against the full key list, keeping its order', () => {
assert.deepEqual(invertSelection(['a', 'b', 'c', 'd'], new Set(['b', 'd'])), ['a', 'c']);
assert.deepEqual(invertSelection(['a', 'b'], []), ['a', 'b']);
assert.deepEqual(invertSelection(['a', 'b'], ['a', 'b']), []);
});
});
describe('blockingLayer', () => {
const c = (blocked, open) => ({ blocked, open });
it('a hidden bait means a browser-level blocker', () => {
assert.equal(blockingLayer({ requests: c(10, 2), cosmetic: c(1, 1) }), 'browser');
assert.equal(blockingLayer({ requests: c(0, 0), cosmetic: c(2, 0) }), 'browser');
});
it('blocked requests with untouched baits mean network level', () => {
assert.equal(blockingLayer({ requests: c(10, 2), cosmetic: c(0, 2) }), 'network');
});
it('nothing blocked anywhere is none', () => {
assert.equal(blockingLayer({ requests: c(0, 12), cosmetic: c(0, 2) }), 'none');
});
it('cannot tell without the cosmetic part or any answer', () => {
assert.equal(blockingLayer({ requests: c(10, 2), cosmetic: c(0, 0) }), null);
assert.equal(blockingLayer({ requests: c(0, 12), cosmetic: c(0, 0) }), null);
assert.equal(blockingLayer({ requests: c(0, 0), cosmetic: c(0, 0) }), null);
});
});
+1
View File
@@ -154,6 +154,7 @@ describe('useShortcuts()', () => {
assert.ok(distinctKeys.has('g'));
assert.ok(distinctKeys.has('o'));
assert.ok(distinctKeys.has('H'));
assert.ok(distinctKeys.has('x'), 'Ad & Tracker Blocking Test is a public tool');
assert.equal(distinctKeys.has('p'), false, 'pulse shortcut stays off without a pulse backend or cloudFlare key');
assert.equal(distinctKeys.has('P'), false, 'persona shortcut stays off a self-hosted instance');
assert.equal(distinctKeys.has('B'), false, 'blocklist shortcut stays off a self-hosted instance');
+31
View File
@@ -81,6 +81,17 @@ const PAYLOADS = {
{ id: 3, ip: '检测失败', country_code: '', org: '检测失败' },
],
},
'adblocktest:finished': {
items: [
{ groupId: 'ads', kind: 'domain', status: 'blocked' },
{ groupId: 'ads', kind: 'domain', status: 'open' },
{ groupId: 'ads', kind: 'domain', status: 'blocked' },
{ groupId: 'social', kind: 'domain', status: 'unknown' },
{ groupId: 'scripts', kind: 'script', status: 'open' },
{ groupId: 'scripts', kind: 'script', status: 'pending' },
{ groupId: 'cosmetic', kind: 'cosmetic', status: 'blocked' },
],
},
'browserinfo:finished': {
userAgent: {
ua: 'Mozilla/5.0 …',
@@ -160,6 +171,26 @@ describe('report builders produce schema-valid sections', () => {
});
describe('cleaning rules', () => {
it('adblocktest keeps per-category request counts only', () => {
const { build } = REPORT_EVENT_BUILDERS['adblocktest:finished'];
// Pending items and cosmetic baits never reach the counts.
assert.deepEqual(build(PAYLOADS['adblocktest:finished']), {
groups: [
{ id: 'ads', total: 3, blocked: 2, open: 1, unknown: 0 },
{ id: 'social', total: 1, blocked: 0, open: 0, unknown: 1 },
{ id: 'scripts', total: 1, blocked: 0, open: 1, unknown: 0 },
],
});
});
it('adblocktest reports nothing when only baits or unanswered items came back', () => {
const { build } = REPORT_EVENT_BUILDERS['adblocktest:finished'];
assert.equal(build({ items: [
{ groupId: 'cosmetic', kind: 'cosmetic', status: 'blocked' },
{ groupId: 'ads', kind: 'domain', status: 'pending' },
] }), null);
});
it('ipinfo drops cards whose ip slot holds a placeholder', () => {
const { build } = REPORT_EVENT_BUILDERS['ipinfo:finished'];
const section = build(PAYLOADS['ipinfo:finished']);
+18
View File
@@ -32,6 +32,10 @@ const makeSections = () => ({
{ id: 'gps-location', axis: 'leak', verdict: 'leak' },
],
},
adblocktest: {
testedAt: '2026-07-14T08:25:00.000Z',
groups: [{ id: 'ads', total: 3, blocked: 2, open: 1, unknown: 0 }],
},
ipinfo: {
testedAt: '2026-07-14T08:00:00.000Z',
cards: [{ source: 'IPCheck.ing IPv4', ip: '1.2.3.4', countryCode: 'US', city: 'LA', timezone: 'America/Los_Angeles', asn: 'AS15169', isp: 'Google' }],
@@ -144,6 +148,20 @@ describe('reportToMarkdown', () => {
assert.ok(md.includes('score: 72/100'));
});
it('renders the adblocktest section as its per-category table', () => {
const report = buildShareReport({
sections: makeSections(),
selectedIds: ['adblocktest'],
maskTail: false,
locale: 'en',
origin: 'ipcheck.ing',
});
assert.deepEqual(validateReport(report), { ok: true, errors: [] });
const md = reportToMarkdown(report, t);
assert.ok(md.includes('| group | total | blocked | notBlocked | noAnswer |'));
assert.ok(md.includes('| ads | 3 | 2 | 1 | 0 |'));
});
it('renders heading, intro, per-section blocks and the closing instruction', () => {
const md = reportToMarkdown(makeReport(), t);
assert.ok(md.startsWith('# report.ai.Heading'));
+5 -1
View File
@@ -95,6 +95,10 @@ const makeValidReport = () => ({
display: { width: 1512, height: 982, pixelRatio: 2 },
connection: null,
},
adblocktest: {
testedAt: '2026-07-14T08:00:00.000Z',
groups: [{ id: 'ads', total: 3, blocked: 2, open: 1, unknown: 0 }],
},
invisibility: {
testedAt: '2026-07-14T08:00:00.000Z',
ip: '1.2.3.4',
@@ -299,7 +303,7 @@ describe('constants', () => {
assert.deepEqual(REPORT_TTL_DAYS, [1, 3, 7]);
assert.ok(REPORT_SECTION_IDS.includes('ipinfo'));
assert.ok(REPORT_SECTION_IDS.includes('enhanceddnsleak'));
assert.equal(REPORT_SECTION_IDS.length, 12);
assert.equal(REPORT_SECTION_IDS.length, 13);
});
});