بنقرة واحدة
awcms-micro-visitor-analytics
Kerjakan bagian mana pun dari epic visitor analytics AWCMS-Micro (Issue
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
Kerjakan bagian mana pun dari epic visitor analytics AWCMS-Micro (Issue
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
| name | awcms-micro-visitor-analytics |
| description | Kerjakan bagian mana pun dari epic visitor analytics AWCMS-Micro (Issue |
Epic visitor analytics (#617-#624) menambah statistik pengunjung manusia
privacy-first untuk rute admin dan publik, di konfigurasi online maupun
offline/LAN. Modul baru visitor_analytics (type: "system") — bukan
diperluas dari reporting/logging yang sudah ada, karena volume,
retensi, dan kontrol privasi visitor telemetry berbeda dari keduanya
(lihat src/modules/visitor-analytics/README.md §Why a separate module).
Skill ini melengkapi (bukan menggantikan) awcms-micro-new-endpoint,
awcms-micro-new-migration, awcms-micro-new-module,
awcms-micro-abac-guard, awcms-micro-sensitive-data (IP/user-agent/geo
adalah data sensitif), awcms-micro-ui-screen (dashboard #622), dan
awcms-micro-observability (retensi/purge, korelasi dengan audit log yang
sudah ada). Skill ini menyediakan konteks cross-cutting epic
spesifik yang menjembatani beberapa issue sekaligus.
| Issue | Scope | Status |
|---|---|---|
| #617 | Module descriptor, permission catalog, config gate | Selesai — lihat §Config di bawah |
| #618 | Visitor session/event/rollup schema + RLS | Selesai — lihat §Schema di bawah |
| #619 | Visitor identity, user-agent, human/bot classification helpers | Selesai — lihat §Domain helpers di bawah |
| #620 | Middleware telemetry collection (admin + public) | Selesai — lihat §Collector di bawah |
| #621 | Analytics API + OpenAPI contract (/api/v1/analytics) | Selesai — lihat §API di bawah |
| #623 | Trusted online geolocation enrichment | Selesai — lihat §Geo enrichment di bawah |
| #622 | Admin visitor analytics dashboard UI (/admin/analytics) | Selesai — lihat §Dashboard UI di bawah |
| #624 | Rollup job, retention purge job, readiness checks, docs pass; REOPENED 2026-07-11 dengan repository audit addendum (default-off, cookie lifetime) | Selesai (termasuk addendum) — lihat §Rollup job, §Retention purge job, §Readiness checks, §Repository audit addendum di bawah |
Urutan dependency (dari body issue masing-masing): 617 → 618 → 619 → 620 →
621 → 622; 623 boleh setelah 620 (paralel dengan 621/622); 624 butuh
617+618+620+621 dan melengkapi 622/623. Epic #617-#624 selesai penuh,
termasuk repository audit addendum #624 (2026-07-11, epic
platform-hardening #679) — lihat §Repository audit addendum di bawah
sebelum menganggap default lama (ENABLED=true, cookie 2 tahun) masih
berlaku.
src/modules/visitor-analytics/domain/visitor-analytics-config.ts)Tujuh belas env var, semuanya opsional dan privacy-first — kalau tidak
di-set sama sekali, config:validate tetap PASS, modul TIDAK aktif
(§Repository audit addendum), dan tidak ada raw IP/raw user-agent/
geolokasi yang tersimpan:
VISITOR_ANALYTICS_ENABLED (default false sejak Issue #624
repository audit addendum, 2026-07-11 — sebelumnya true di rilis
awal Issue #617) — master switch. Deployment existing yang sudah
men-set var ini true eksplisit tidak terdampak; lihat §Repository
audit addendum untuk migration note lengkap.VISITOR_ANALYTICS_MODE (default basic) — enum basic | detailed
(VISITOR_ANALYTICS_MODES, isKnownVisitorAnalyticsMode). Nilai tak
dikenal jatuh ke basic, tidak pernah throw.VISITOR_ANALYTICS_COLLECT_ADMIN / _COLLECT_PUBLIC / _COLLECT_API —
toggle per permukaan (default true/true/false).VISITOR_ANALYTICS_DETAILED_ENABLED — cadangan mode detailed (default
false, belum dikonsumsi apa pun).VISITOR_ANALYTICS_RAW_IP_ENABLED / _RAW_USER_AGENT_ENABLED /
_GEO_ENABLED — default false semuanya, ini inti privacy-first
gate-nya. Independen dari VISITOR_ANALYTICS_MODE — mode tidak pernah
menyalakan ketiganya secara implisit.VISITOR_ANALYTICS_TRUST_PROXY / _TRUST_CLOUDFLARE — default false;
sama prinsip keamanan dengan PUBLIC_TRUST_PROXY (skill
awcms-micro-tenant-domain-routing) — hanya true di belakang proxy
tepercaya yang benar-benar mengisi ulang header, bukan meneruskan nilai
klien.VISITOR_ANALYTICS_ONLINE_WINDOW_SECONDS
(300), _EVENT_RETENTION_DAYS (90), _RAW_DETAIL_RETENTION_DAYS (30),
_ROLLUP_RETENTION_DAYS (730), _VISITOR_KEY_COOKIE_TTL_DAYS (30,
Issue #624 repository audit addendum — sebelumnya hardcoded
~2 tahun/63_072_000 detik di src/middleware.ts, bukan env var sama
sekali) — divalidasi parsePositiveInt, wajib integer positif bila
diisi; tak diisi → default di atas, tidak pernah gagal validasi.VISITOR_ANALYTICS_HASH_SALT (default "") — salt untuk fingerprint
visitor pseudonymous, dikonsumsi domain/visitor-key.ts's
hashVisitorKey/hashIpAddress/hashUserAgent (Issue #619) — lihat
§Domain helpers di bawah. Belum ada caller yang benar-benar memanggil
fungsi hashing ini dengan nilai request nyata sampai middleware #620.Entry point: resolveVisitorAnalyticsConfig(env) — SATU fungsi yang
harus dipanggil issue lanjutan (#619 helper, #620 middleware), jangan
re-derive baca process.env.VISITOR_ANALYTICS_* langsung. Gate boolean
tunggal: isVisitorAnalyticsEnabled(env).
Validasi: checkVisitorAnalyticsConfig() (scripts/validate-env.ts),
dipanggil dari runEnvValidation(). Didokumentasikan di
docs/awcms-micro/18_configuration_env_reference.md §Visitor analytics.
Test: tests/unit/visitor-analytics-config.test.ts,
tests/validate-env.test.ts's describe("checkVisitorAnalyticsConfig", ...).
src/modules/visitor-analytics/module.ts)Modul visitor_analytics terdaftar di src/modules/index.ts's
listModules(). Hanya descriptor + config — tidak ada tabel/API/UI/
middleware di sini, itu semua issue lanjutan.
key: "visitor_analytics", type: "system" (bukan "domain") —
observability infrastructure yang dipakai bersama semua tenant, sama
alasan dengan reporting/logging/tenant_domain.dependencies: ["tenant_admin", "identity_access", "logging", "reporting"]
(persis daftar di body issue #617).api: { basePath: "/api/v1/analytics", openApiPath: "openapi/awcms-micro-public-api.openapi.yaml" } dan navigation: [{ path: "/admin/analytics", requiredPermission: "visitor_analytics.dashboard.read", order: 70 }] dideklarasikan sejak
Issue #617 meski saat itu API (#621)/dashboard (#622) belum ada —
konvensi sama dengan tenant_domain's descriptor sebelum #562. Kedua
issue itu sekarang sudah selesai (lihat §API dan §Dashboard UI di
bawah), jadi navigation/api di atas bukan lagi janji ke depan.permissions: 8 entry (dashboard.read, realtime.read,
sessions.read, events.read, raw_detail.read, settings.read,
settings.update, retention.purge), match persis seed migration 038
— divalidasi tests/modules/visitor-analytics-module.test.ts.
raw_detail.read sengaja terpisah dari dashboard.read — operator
bisa memberi akses dashboard agregat tanpa memberi akses PII mentah
(IP/user-agent).settings/jobs/health — belum ada
settings-API/job/health-check nyata untuk didokumentasikan (konsisten
konvensi module_management/README.md, lihat juga precedent
tenant_domain Issue #558).application//infrastructure/api yang dibuat di
Issue #617 — hanya module.ts + domain/visitor-analytics-config.ts +
README.md. Folder lain menyusul issue yang benar-benar butuh
(#618 schema, #619 helpers, #620 middleware, #621 API).Permission migration: sql/038_awcms_micro_visitor_analytics_permissions.sql
— pola sama sql/032_awcms_micro_tenant_domain_permissions.sql (INSERT ... ON CONFLICT (module_key, activity_code, action) DO NOTHING), belum ada
role/access assignment yang memakainya.
sql/039_awcms_micro_visitor_analytics_schema.sql)Tiga tabel tenant-scoped, semua ENABLE+FORCE ROW LEVEL SECURITY dengan
policy standar tenant_id = current_setting('app.current_tenant_id')::uuid.
Schema saja — belum ada writer: middleware (#620) belum menulis apa
pun ke sini.
awcms_micro_visitor_sessions — satu baris per sesi presence. area IN ('admin','public','api','auth','setup','unknown'), device_type IN ('desktop','mobile','tablet','bot','unknown') (nullable). ip_address
(raw inet) nullable, hanya diisi kalau
VISITOR_ANALYTICS_RAW_IP_ENABLED=true. login_identifier_snapshot
nullable, tidak boleh diisi untuk pengunjung publik anonim.awcms_micro_visit_events — satu baris per page-view/API call.
human_status IN ('human','bot','unknown'), status_code (nullable)
wajib 100-599 bila diisi. Dua kolom jsonb catch-all
(user_agent_parsed, geo) default '{}'::jsonb — hanya untuk nilai
hasil parse (Issue #619/#623), tidak pernah raw request data.awcms_micro_visitor_daily_rollups — PRIMARY KEY (tenant_id, date, area), sekaligus target upsert job rollup (#624). Tidak ada index
terpisah (tenant_id, date, area) — PK sudah menyediakannya, index
redundan yang diminta issue sengaja tidak dibuat.Tidak ada deleted_at/soft delete di tiga tabel ini (bukan master/config
data) — lifecycle-nya purge berbasis retensi (job Issue #624), pola sama
awcms_micro_audit_events (migration 011).
Test: tests/integration/visitor-analytics-schema.integration.test.ts —
constraint check per tabel, RLS isolation lintas tenant, fail-closed tanpa
GUC, dan scan kolom memastikan tidak ada kolom berbentuk secret (password/
token/cookie/authorization/request_body).
Docs: docs/awcms-micro/04_erd_data_dictionary.md §Visitor Analytics
(ERD ringkas) dan §Retention awal (dua baris retensi baru).
Temuan security-auditor Issue #618 (Medium, DITUTUP di Issue #620 — lihat
§Collector di bawah): FK biasa
(identity_id uuid REFERENCES awcms_micro_identities (id),
visitor_session_id uuid REFERENCES awcms_micro_visitor_sessions (id))
tidak dilindungi RLS — constraint-check FK PostgreSQL berjalan dengan
privilege owner tabel, bukan role pemanggil (dokumentasi resmi
CREATE POLICY: "foreign key references are not restricted by row
security"). Kalau nilai identity_id/visitor_session_id yang ditulis
Issue #620 pernah berasal dari input yang dikontrol client (bukan
di-derive dari session/tenant context di server), ini jadi existence
oracle lintas tenant (attacker bisa binary-search UUID buat tahu
"row ini ada di suatu tenant" vs "row ini tidak ada sama sekali", lepas
dari RLS). Wajib dipertahankan Issue #620: identity_id selalu
di-derive dari identity sesi terautentikasi milik request itu sendiri
(tidak pernah dari field yang bisa diisi client), visitor_session_id
selalu di-resolve server-side dari cookie/token opaque yang di-lookup di
dalam tenant context pemanggil sendiri (tidak pernah dari raw UUID
client yang langsung dicocokkan ke FK). Tambahkan test integrasi di #620
yang membuktikan UUID palsu/lintas-tenant di salah satu field ditolak
sebelum sempat menyentuh FK.
src/modules/visitor-analytics/domain/)Lima file, semua pure/hampir-pure. application/collector.ts (Issue
#620) adalah caller pertama — jangan re-derive logic ini di issue
lanjutan mana pun; import langsung.
visitor-key.ts — generateVisitorKey/isValidVisitorKey/
resolveVisitorKey (cookie anonim, tolak nilai forged/non-UUID) dan
hashVisitorKey/hashIpAddress/hashUserAgent (HMAC-SHA256 di-key
dengan VISITOR_ANALYTICS_HASH_SALT — beda dari hashIdentifier
profile-identity yang sengaja unsalted, lihat file ini untuk alasan
lengkap).user-agent.ts — parseUserAgent/isBotUserAgent. Tabel regex
ringkas (bukan dependency npm) untuk browser (Chrome/Firefox/Safari/
Edge/Opera/Samsung Internet/IE), OS (Windows/macOS/iOS/Android/Chrome
OS/Linux — urutan cek OS penting: iOS/Android/CrOS dicek sebelum
Windows/macOS/Linux karena UA iPhone/iPad selalu membawa token
kompatibilitas "like Mac OS X" dan UA Android/CrOS dibangun di atas
string kernel Linux — cek yang lebih spesifik dulu mencegah salah
klasifikasi), device type, dan ~30 signature bot/crawler/social-preview/
automation-client. Bukan titik keputusan otorisasi/keamanan — UA
bisa dipalsukan trivial.human-classifier.ts — classifyHumanStatus (tri-state
human/bot/unknown untuk visit_events.human_status: bot UA selalu
menang; sesi terautentikasi + UA apa pun non-bot = human; request
anonim + UA tak dikenal = unknown, tidak pernah default human) dan
classifySessionHumanity (boolean is_human/bot_reason untuk
visitor_sessions, yang tidak punya kolom tri-state).path-sanitizer.ts — sanitizePath (buang 11 query param sensitif
minimum issue: token/code/password/secret/email/phone/authorization/
access_token/refresh_token/reset_token/mfaChallengeToken, case-
insensitive) dan isTrackablePath (exclude static asset, /_astro/*,
favicon, endpoint health, path spec OpenAPI/AsyncAPI dari hitungan
pageview). Fail SAFE, bukan fail open (post-review fix PR #627):
input yang gagal di-parse URL() membuang seluruh query string
(rawPath.split("?")[0]), bukan echo raw input — versi awal echo raw
input, yang bisa membocorkan query param sensitif yang justru gagal
di-parse (mis. IPv6 literal rusak).referrer.ts — extractReferrerDomain (hostname saja, tidak pernah
path/query/fragment, null untuk scheme non-http(s)).Test: tests/unit/visitor-analytics-{visitor-key,user-agent,human-classifier,path-sanitizer,referrer}.test.ts
— user-agent.test.ts mencakup 20+ contoh UA nyata (desktop/mobile/
tablet/13 signature bot/unknown).
application/collector.ts + src/middleware.ts)Satu-satunya writer awcms_micro_visitor_sessions/awcms_micro_visit_events.
src/middleware.ts memanggilnya setelah next() resolve (di kedua
cabang pre-admin dan /admin/*), jadi response.status sudah diketahui.
shouldCollectRequest (pure, unit-tested) — cek
config.enabled → isTrackablePath → flag per-area
(COLLECT_ADMIN/COLLECT_API/COLLECT_PUBLIC). /login diklasifikasi
public (page render), bukan auth — auth/setup khusus untuk
/api/v1/auth/*//api/v1/setup/*, sama-sama digerbangi COLLECT_API
(domain/request-area.ts's determineArea)./admin/* pakai ssrContext.tenantId yang sudah
ada (redirect guard sudah jalan lebih dulu). Area lain pakai
resolvePublicTenantFromRequest (#559) — best-effort, tenant tidak
resolve = tidak dikoleksi, bukan hard failure.awcms_micro_visitor_key, httpOnly+sameSite=lax,
di-set hanya saat request benar-benar dikoleksi. Diubah Issue #624
repository audit addendum: maxAge sekarang configurable
(VISITOR_ANALYTICS_VISITOR_KEY_COOKIE_TTL_DAYS, 30 hari default —
bukan lagi hardcoded 2 tahun), dan src/middleware.ts memanggil
shouldRevokeVisitorKeyCookie/planVisitorKeyCookie
(domain/visitor-key-cookie.ts, fungsi pure baru) SEBELUM gate
path/area: cookie yang sudah ada dihapus aktif begitu
VISITOR_ANALYTICS_ENABLED bukan "true", dan tidak ada cookie/event
baru yang pernah ditulis selama modul nonaktif. Jangan re-derive logic
set/delete cookie langsung di src/middleware.ts lagi — kedua fungsi
pure itu satu-satunya tempat keputusannya.(tenant_id, visitor_key_hash, area) (index baru migration 040). Dalam window
VISITOR_ANALYTICS_ONLINE_WINDOW_SECONDS → reuse row, tapi UPDATE
hanya jika tulisan terakhir ≥30 detik lalu (write-throttle). Di luar
window → session baru. Event tidak pernah di-throttle — selalu satu
baris per request yang dikoleksi. login_identifier_snapshot sengaja
selalu null (deferred, bukan regresi — lihat README modul).collectVisitorTelemetry tidak pernah throw — semua
error ditangkap, dicatat log("warning", "visitor_analytics.collector.failed", ...)
dengan correlationId, tidak pernah membocorkan data sensitif.
withTenant dipanggil dengan workClass: "background_sync" (prioritas
DB terendah, doc 16).identityId selalu dari ssrContext.identityId (server-derived,
hanya ada setelah redirect guard /admin/* lolos) atau null untuk
publik — tidak pernah dari input client. visitor_session_id selalu
dari row yang baru saja ditemukan/dibuat fungsi ini sendiri di dalam
transaksi tenant-scoped-nya sendiri — tidak pernah dari UUID mentah
client.domain/client-ip.ts's resolveAnalyticsClientIp —
saudara lib/security/rate-limit.ts's resolveClientIp yang lebih
konservatif; hanya percaya CF-Connecting-IP/X-Forwarded-For saat
VISITOR_ANALYTICS_TRUST_CLOUDFLARE/_TRUST_PROXY eksplisit true.Test: tests/unit/visitor-analytics-collector.test.ts (shouldCollectRequest),
tests/unit/visitor-analytics-request-area.test.ts,
tests/unit/visitor-analytics-client-ip.test.ts,
tests/integration/visitor-analytics-collector.integration.test.ts (9
test: create, sanitize, bot classify, raw-IP opt-in, throttle+reuse,
session rollover, non-trackable no-op, fail-open invalid tenant,
authenticated admin). Juga smoke-test manual lewat dev server + Postgres
nyata (setup wizard → login → /admin → row session/event benar; / →
session publik; asset statis dan /api/v1/health default-off → nihil).
src/pages/api/v1/analytics/*)11 endpoint. Semua wajib bearer session + tenant context + authorizeInTransaction
(ABAC default-deny) — deny selalu 403 ACCESS_DENIED, tidak pernah data
analytics kosong/nol diam-diam.
GET /realtime (realtime.read), GET /summary|pages|devices|locations|security
(dashboard.read, range=24h|7d|30d|12m divalidasi ketat →
400 VALIDATION_ERROR untuk nilai lain).GET /sessions (sessions.read), GET /events (events.read) — keyset
pagination (limit 50), field raw detail (ipHash/ipAddress/
userAgentHash/loginIdentifierSnapshot) null kecuali caller juga
punya raw_detail.read (dicek via auth.grantedPermissionKeys.has(...)
setelah guard utama lolos — domain/analytics-response-shaping.ts).GET/PATCH /settings (settings.read/.update) — reuse langsung
storage generik Module Management (awcms_micro_module_settings, Issue
#516) via fetchModuleSettingsView/updateModuleSettings, tapi
di-gate permission visitor_analytics.* sendiri (bukan
module_management.settings.* yang dipakai endpoint generiknya). Jangan
bangun storage settings kedua untuk modul ini.POST /retention/purge (retention.purge) — wajib Idempotency-Key,
audit critical. Logic nyata (bukan stub) di
application/retention-purge.ts: hapus event > EVENT_RETENTION_DAYS,
clear ip_address/login_identifier_snapshot sesi > RAW_DETAIL_RETENTION_DAYS
(row tetap), hapus sesi > EVENT_RETENTION_DAYS (aman dijalankan
setelah hapus event — last_seen_at sesi selalu >= occurred_at
event-nya sendiri, jadi FK visit_events.visitor_session_id sudah
bersih), hapus rollup > ROLLUP_RETENTION_DAYS. Issue #624's scheduled
job selesai — bun run analytics:purge
(scripts/visitor-analytics-purge.ts, via purgeVisitorAnalyticsForAllTenants)
memanggil purgeVisitorAnalyticsData ini langsung untuk setiap tenant
active, tidak re-derive. Lihat §Rollup job/§Retention purge job di
bawah untuk detail penuh.application/analytics-queries.ts) baca langsung dari
visit_events/visitor_sessions mentah, bukan visitor_daily_rollups
— tetap begitu setelah #624: rollup job (di bawah) kini mengisi tabel
itu, tapi switch fetchAnalyticsSummary membaca rollup untuk rentang
lama tetap open future work (optimisasi performa, bukan bagian
acceptance criteria #624).Test: tests/unit/visitor-analytics-{range,response-shaping}.test.ts,
tests/integration/visitor-analytics-api.integration.test.ts (13 test:
auth guard, ABAC deny lintas endpoint, realtime, summary+range validation,
raw-detail gating dua arah, keyset pagination 55-row, FK-straddle purge,
geo/jsonb object shape lewat API nyata, settings GET/PATCH + secret-key
rejection, retention purge idempotency+delete+audit).
domain/geo-enrichment.ts)Country code saja, dari header Cloudflare CF-IPCountry — tidak
pernah external network call. Gate ganda: VISITOR_ANALYTICS_GEO_ENABLED
dan VISITOR_ANALYTICS_TRUST_CLOUDFLARE harus true keduanya;
salah satu false → semua field null. Region/city/timezone selalu
null di issue ini (belum ada local GeoIP DB, out of scope).
src/middleware.ts panggil resolveGeoEnrichment di samping
resolveAnalyticsClientIp, hasilnya masuk collectVisitorTelemetry's
field baru geo (Issue #620's collector.ts di-extend, bukan file
baru) — ditulis ke visitor_sessions.country_code/region/city/timezone
DAN visit_events.geo (shape sama yang sudah dibaca
fetchTopCountries, Issue #621 — GET /api/v1/analytics/locations
otomatis dapat data nyata begitu geo enrichment aktif, tanpa ubah sisi
API).
Hardening ambiguous-header (domain/client-ip.ts, issue ini juga):
resolveAnalyticsClientIp sekarang menolak X-Forwarded-For/
CF-Connecting-IP yang punya >1 nilai comma-separated (anomali, log
warning, fallback ke sumber berikutnya — sama persis pola
X-Forwarded-Host di public-host-tenant-resolver.ts, epic
tenant-domain-routing). Proxy tepercaya yang benar wajib overwrite,
bukan append, header ini (kontrak sama PUBLIC_TRUST_PROXY, doc 18)
— jadi proxy yang dikonfigurasi benar tidak pernah menghasilkan >1 nilai
di sini juga.
Temuan post-review issue ini — bug jsonb laten sejak #620:
collector.ts's user_agent_parsed/geo sebelumnya ditulis via
JSON.stringify(...)::jsonb — bytes yang tersimpan identik dengan
${object}::jsonb, TAPI Bun.SQL men-decode SELECT berikutnya dari
kolom itu sebagai raw JSON string, bukan object ter-parse (lihat
memory bun-sql-jsonb-stringify-trap untuk bukti empiris lengkap).
Ini diam-diam memengaruhi response GET /api/v1/analytics/events's
userAgentParsed/geo sejak #621 rilis. Wajib dipertahankan issue
lanjutan: selalu bind object JS polos sebagai parameter query untuk
kolom jsonb (${plainObject}::jsonb), jangan pernah
JSON.stringify() dulu — cek grep -rn "JSON.stringify.*::jsonb" di
modul manapun yang disentuh sebelum menganggap kolom jsonb "sudah
benar".
Test: tests/unit/visitor-analytics-{client-ip,geo-enrichment}.test.ts,
tests/integration/visitor-analytics-collector.integration.test.ts
(geo tertulis ke session+event, geo kosong tetap null),
tests/integration/visitor-analytics-api.integration.test.ts (geo/
userAgentParsed object nyata lewat /events dan /locations).
src/pages/admin/analytics.astro)/admin/analytics, gate visitor_analytics.dashboard.read. UI-only —
tidak menambah endpoint/permission baru, tidak pernah query
awcms_micro_visitor_sessions/awcms_micro_visit_events langsung. Semua
angka/tabel dimuat client-side dari GET /api/v1/analytics/* (#621) lewat
helper baru fetchJson (src/lib/ui/admin-form-client.ts) — SENGAJA
bukan konvensi SSR-panggil-application-layer-langsung yang dipakai
admin/security.astro/admin/sync.astro, supaya ABAC server-side
(authorizeInTransaction) tetap satu-satunya enforcement point nyata.
domain/analytics-response-shaping.ts sudah
men-null-kan ipAddress/ipHash/userAgentHash/
loginIdentifierSnapshot untuk caller tanpa raw_detail.read); logic
UI baru (domain/dashboard-view.ts's displayOrPlaceholder/
buildSessionRowCells) cuma menerjemahkan null jadi placeholder
aman, tidak pernah membuat keputusan izin sendiri. showRawDetailColumns
cuma menyembunyikan 4 kolom tabel sebagai kosmetik (biar caller yang
memang tidak akan pernah lihat nilai asli tidak melihat tembok tanda
strip) — tidak bisa bocor karena nilai yang ditampilkan selalu berasal
dari respons API apa adanya.area/tipe pengunjung, jadi
kedua filter ini cuma menyaring baris tabel active-sessions yang sudah
ter-fetch (client-side, matchesAreaFilter/matchesVisitorTypeFilter).
Hanya range (24h|7d|30d|12m) yang benar-benar query parameter API
nyata (/summary, /pages, /devices, /locations, /security).
Jangan bangun ulang asumsi "semua filter berlaku ke semua section" di
issue lanjutan manapun tanpa lebih dulu menambah parameter itu ke API
(perubahan API, di luar scope #622).StateNotice disabled
kalau resolveVisitorAnalyticsConfig().geoEnabled && .trustCloudflare tidak keduanya true (baca config env langsung,
bukan akses DB, bukan gerbang keamanan kedua — cuma keputusan render).Test: tests/unit/visitor-analytics-dashboard-view.test.ts (raw-detail-
null formatting, section-state loading/empty/error, filter predicate —
semua pure), tests/e2e/admin-analytics-access-denied.e2e.ts,
tests/e2e/admin-analytics-dashboard.e2e.ts (raw-detail gating end-to-end
lewat render browser sungguhan — caller tanpa raw_detail.read melihat
baris sesi tenant yang sama tanpa kolom raw-detail maupun nilai
sha256:-prefixed apa pun).
application/rollup.ts + scripts/visitor-analytics-rollup.ts)bun run analytics:rollup — mengagregasi awcms_micro_visit_events
mentah menjadi awcms_micro_visitor_daily_rollups, satu baris per
(tenant, date, area), untuk setiap tenant active.
rollupVisitorAnalyticsForDate
merekomputasi PENUH dari event mentah untuk tanggal yang diminta dan
UPSERT (ON CONFLICT (tenant_id, date, area) DO UPDATE SET ... = EXCLUDED...) — rerun tanggal yang sama berkali-kali selalu konvergen
ke baris yang sama, tidak pernah menambah ke nilai lama. Jangan ubah
ini jadi logic increment/append apa pun.application/analytics-queries.ts's top-N
helper verbatim — fungsi-fungsi itu cumulative-since-start (tanpa
batas atas) dan tenant-wide (tanpa filter area), bentuk yang dipakai
dashboard live (#621). Rollup butuh jendela [dayStart, dayEnd)
tertutup DAN split per area (PK tabel rollup), jadi
application/rollup.ts mengimplementasikan query top-N sendiri yang
di-scope hari+area, mengikuti pola keamanan SQL yang sama
(allow-list kolom/key jsonb sebelum tx.unsafe).--date=YYYY-MM-DD | --start-date=.../--end-date=... | default
"kemarin" (UTC).Test: tests/integration/visitor-analytics-rollup.integration.test.ts.
scripts/visitor-analytics-purge.ts)bun run analytics:purge — mengiterasi setiap tenant active dan
memanggil purgeVisitorAnalyticsData (§API di atas) LANGSUNG lewat
fungsi yang diekspor purgeVisitorAnalyticsForAllTenants, TIDAK PERNAH
re-derive empat cutoff-nya. Hanya tenant yang benar-benar ada
baris terhapus/terbersihkan yang mendapat audit critical
retention_purged (attributes: empat angka ringkasan saja) — tenant
tanpa data kedaluwarsa tidak menghasilkan audit noise. Tidak ada lapisan
batching tambahan di atas apa yang purgeVisitorAnalyticsData sudah
lakukan.
Test: tests/integration/visitor-analytics-purge.integration.test.ts —
SENGAJA tidak menguji ulang empat aturan retensi (sudah dicakup
tests/integration/visitor-analytics-api.integration.test.ts lewat
POST .../retention/purge, fungsi yang sama persis); hanya menguji apa
yang ditambahkan script ini — iterasi multi-tenant, jumlah total lintas
tenant, dan audit hanya untuk tenant yang punya efek nyata.
scripts/security-readiness.ts)scripts/validate-env.ts's checkVisitorAnalyticsConfig (#617) TETAP
shape-only (enum/positive-int). Lima check SAFETY cross-field baru ada
di security-readiness.ts (bukan validate-env.ts) — pola yang sama
checkOnlineAuthSecurityConfig/Ready dkk. sudah pakai (shape vs
safety/severity). Semua reuse resolveVisitorAnalyticsConfig:
| Check | Severity | Gagal saat |
|---|---|---|
checkVisitorAnalyticsRawIpRetentionReady | critical | Raw IP aktif + retensi raw detail > retensi event |
checkVisitorAnalyticsRawUserAgentRetentionReady | warning | Raw user-agent aktif (masih no-op) + retensi sama tidak aman |
checkVisitorAnalyticsGeoTrustedSourceReady | critical | Geo aktif tanpa VISITOR_ANALYTICS_TRUST_CLOUDFLARE |
checkVisitorAnalyticsRetentionOrderingReady | warning | Retensi raw detail > event, ATAU rollup < event |
checkVisitorAnalyticsHashSaltReady | warning | Modul aktif + VISITOR_ANALYTICS_HASH_SALT kosong |
checkVisitorAnalyticsVisitorKeyCookieTtlReady | warning | Modul aktif + VISITOR_ANALYTICS_VISITOR_KEY_COOKIE_TTL_DAYS > 400 hari (Issue #624 repository audit addendum) |
Default privacy-first (semua var tidak di-set) lulus BERSIH keenam
check ini. Hanya critical yang memblokir go-live. Jangan re-derive
aturan ini di validate-env.ts — kalau butuh dipakai script lain,
import dari security-readiness.ts seperti check lain sudah lakukan.
Test: tests/security-readiness.test.ts.
Setelah Issue #624's scope asli (rollup/purge/readiness/docs) selesai, audit repositori 2026-07-11 menemukan dua celah default privasi dan menambah scope ke issue yang SAMA (bukan issue baru):
.env.example mengaktifkan analytics secara default — DITUTUP:
VISITOR_ANALYTICS_ENABLED sekarang default false di
VISITOR_ANALYTICS_DEFAULTS (domain/visitor-analytics-config.ts),
.env.example, dan src/lib/config/registry.ts sekaligus (ketiganya
harus tetap sinkron — tests/unit/config-docs-check.test.ts's
runConfigDocsCheck menegakkan kehadiran var yang sama di ketiganya,
BUKAN kesamaan nilai default). Deployment existing yang sudah men-set
var ini true eksplisit tidak terdampak — hanya deployment yang
mengandalkan default implisit lama yang perlu menambahkan var ini
secara eksplisit setelah upgrade. Tidak ada migration data — perubahan
murni di layer config.visitorKeyCookieTtlDays (default 30, env
VISITOR_ANALYTICS_VISITOR_KEY_COOKIE_TTL_DAYS) di
VisitorAnalyticsConfig, dikonsumsi
resolveVisitorKeyCookieMaxAgeSeconds. Dua fungsi pure baru di
domain/visitor-key-cookie.ts — shouldRevokeVisitorKeyCookie
(dipanggil SEBELUM gate shouldCollectRequest, true hanya saat modul
nonaktif DAN cookie valid masih ada → middleware menghapusnya) dan
planVisitorKeyCookie (dipanggil SESUDAH gate lolos, HANYA saat modul
aktif — selalu resolve nilai + tandai perlu Set-Cookie atau tidak,
mempertahankan invarian lama "cookie di-set hanya saat request
benar-benar dikoleksi", bukan di setiap request).collectRequestAnalytics
(src/middleware.ts) mengecek config.enabled SEBELUM apa pun lain
(setelah revocation check di atas) dan return langsung bila mati —
tidak ada jalur kode yang bisa sampai ke context.cookies.set/
collectVisitorTelemetry saat modul nonaktif. Diverifikasi
tests/unit/visitor-analytics-visitor-key-cookie.test.ts dan
tests/unit/visitor-analytics-collector.test.ts (kasus
VISITOR_ANALYTICS_DEFAULTS disabled-by-default).docs/awcms-micro/visitor-analytics.md (§Default opt-in dan upgrade
path, §Cookie anonim, baris baru tabel UU PDP, bagian ISO/IEC
27701:2025 dimutakhirkan dari referensi 2019 sebelumnya).Yang TIDAK berubah (di luar scope addendum, jangan disentuh issue lanjutan tanpa alasan baru): raw IP/raw user-agent/geo tetap default mati (sudah begitu sejak #617, addendum hanya menegaskan ulang), tidak ada perubahan skema/tabel (migration baru TIDAK dibutuhkan — perubahan ini murni config + middleware), tidak ada endpoint API baru.
Test baru: tests/unit/visitor-analytics-visitor-key-cookie.test.ts
(pure, shouldRevokeVisitorKeyCookie/planVisitorKeyCookie). Test yang
diperbarui (bukan baru) untuk mencerminkan default baru:
tests/unit/visitor-analytics-config.test.ts,
tests/unit/visitor-analytics-collector.test.ts,
tests/security-readiness.test.ts.
VISITOR_ANALYTICS_MODE=detailed atau flag lain; wajib
opt-in eksplisit per flag masing-masing.raw_detail.read terpisah dari dashboard.read — endpoint/UI yang
menampilkan IP/user-agent mentah wajib cek permission raw_detail.read
secara eksplisit, bukan cukup dashboard.read.ENABLE+FORCE, sama pola semua tabel tenant-scoped lain di repo ini
(lihat docs/adr/0003-postgresql-rls-multi-tenant.md). Issue
lanjutan yang menulis ke tabel ini (mis. #620) tetap wajib lewat
withTenant, tidak pernah query langsung tanpa tenant context.VISITOR_ANALYTICS_HASH_SALT bukan credential provider — dipakai
untuk fingerprint pseudonymous (dedup visitor tanpa cookie persisten),
bukan untuk autentikasi apa pun. Jangan expose di response/log/audit.VISITOR_ANALYTICS_ENABLED default false. Jangan
pernah membalik ini kembali ke true tanpa keputusan sadar yang
didokumentasikan ulang — ini adalah keputusan privasi/kepatuhan
eksplisit, bukan preferensi teknis.VISITOR_ANALYTICS_VISITOR_KEY_COOKIE_TTL_DAYS
kembali ke orde tahun tanpa justifikasi eksplisit, dan jangan
menghapus logic shouldRevokeVisitorKeyCookie (revocation saat modul
nonaktif) — keduanya bagian dari keputusan privasi addendum ini, bukan
detail implementasi yang bebas diubah.src/modules/visitor-analytics/README.md — detail per-issue di dalam modul.docs/awcms-micro/visitor-analytics.md — panduan operasional lengkap
(mode offline/LAN vs online vs trusted-proxy/Cloudflare, retensi,
rollup/purge, dan pemetaan kepatuhan UU PDP/PP PSTE/ISO
27001-27002-27005-27701/OWASP ASVS/Logging Cheat Sheet).docs/awcms-micro/18_configuration_env_reference.md §Visitor analytics.docs/awcms-micro/20_threat_model_security_architecture.md §Standar
tambahan dipicu epic visitor analytics.AGENTS.md skill table.Triase dan perbaiki temuan CodeQL code scanning AWCMS-Micro (github.com/ahliweb/awcms-micro/security/code-scanning). Gunakan saat diminta "analisis code scanning"/"perbaiki CodeQL", saat sebuah PR gagal check CodeQL, atau saat menemukan alert baru. Mendokumentasikan enam false-positive nyata yang sudah ditemukan (name-heuristic password, incompatible-types typeof/null, URL substring-sanitization di test mock, dua kasus dismiss resmi tanpa reformulasi kode, Bun.SQL tagged-template null-cast, dan build-time extension seam trivial-conditional) plus pola "unused-local-variable di test kadang menandai coverage gap" — supaya tidak diinvestigasi ulang dari nol.
Scaffold modul baru pada modular monolith AWCMS-Micro. Gunakan saat membuat modul domain baru di src/modules/ (mis. warehouse-management, accounting-tax) atau saat memerlukan struktur module.ts + domain/application/infrastructure/api + README. Ikuti struktur standar doc 10 & 11.
Terapkan kontrol akses RBAC+ABAC default-deny plus RLS pada endpoint/service AWCMS-Micro. Gunakan pada setiap endpoint non-public dan setiap akses data tenant-scoped. Menegakkan default deny, deny-overrides-allow, decision log, dan tenant isolation sesuai doc 03 & 10.
Tulis audit log untuk aksi high-risk AWCMS-Micro dengan redaction. Gunakan pada login, access assignment, profile merge, price change, transaction posted/cancel/return, stock adjustment, warehouse transfer, Coretax export, sync conflict resolution, AI tool call, dan security readiness decision. Sesuai doc 03 & 10.
Terapkan idempotency pada mutation high-risk AWCMS-Micro agar aman dari double-submit. Gunakan saat implementasi posting transaksi, cancel/return, transfer approve/ship/receive, cycle count, adjustment, VAT generate, Coretax batch, receipt send, sync push, atau workflow decision. Sesuai doc 10.
Kelola/konsumsi sistem Module Management AWCMS-Micro (registry, komposisi modul build-time untuk aplikasi turunan, manifest kompatibilitas aplikasi turunan, tenant lifecycle enable/disable, settings, permission sync/status, navigation, job registry, health/readiness). Gunakan saat menambah field descriptor baru (permissions/navigation/settings/jobs/health) di modul lain, saat menyelidiki kenapa suatu modul terlihat degraded/orphaned, saat aplikasi turunan perlu menyusun modulnya sendiri lewat `src/modules/application-registry.ts` tanpa mengedit registry base, saat aplikasi turunan perlu memverifikasi kompatibilitasnya dengan rilis base terbaru (`bun run extension:check`), atau saat mengubah perilaku enable/disable/settings/health module_management sendiri. Sesuai src/modules/module-management/README.md, epic