Skip to content

feat: hostdoc insight <code> — CloudFront visit stats (#43, cloudfront slice) - #53

Merged
jkas2016 merged 14 commits into
mainfrom
feat/43-cloudfront-insight
Aug 7, 2026
Merged

feat: hostdoc insight <code> — CloudFront visit stats (#43, cloudfront slice)#53
jkas2016 merged 14 commits into
mainfrom
feat/43-cloudfront-insight

Conversation

@jkas2016

Copy link
Copy Markdown
Owner

Summary

Adds hostdoc insight <code> — per-document page-view hits + approximate unique visitors for cloudfront-mode sites, read from CloudFront standard-logging (v2) access logs provisioned in the user's own AWS. This is the cloudfront vertical slice of #43; s3-website and self-hosted are deferred to their own future slices.

No HTML is modified (no JS beacon), no standing server/endpoint — logs live in the user's S3 and are parsed CLI-side.

Design

Spec: docs/superpowers/specs/2026-07-11-cloudfront-insight-design.md · Plan: docs/superpowers/plans/2026-07-11-cloudfront-insight.md

  • Logging = standard logging v2 (CloudWatch vended-logs delivery), not v1 legacy. Rationale: v1 legacy requires S3 ACLs on the log bucket, which fights hostdoc's hardened private-OAC bucket model ([Enhancement] Terraform infra 하드닝 (route53·IAM·validation·description) #20); v2 is bucket-policy based (ACL-free) and supports field selection (log only the minimal set) + partitioning.
  • Dedicated private log bucket (separate from content), 30-day S3 lifecycle expiry, delivery via delivery.logs.amazonaws.com bucket policy. Partitioned {DistributionId}/{yyyy}/{MM}/{dd}/{HH}.
  • Pipeline: list log objects → gunzip → parse W3C TSV (dynamic #Fields mapping) → filter to /<code>/ page views (.html//, assets excluded) → aggregate hits + unique client IPs (bots + non-2xx/3xx filtered).

Changes

  • New pure modules: src/lib/cflog.ts (W3C parser), src/lib/insight.ts (aggregation + log-key date selection).
  • src/lib/aws.ts: getObjectBytes (raw bytes for gzip).
  • src/lib/config.ts + src/commands/init.ts: optional cloudfront logBucket, threaded through both cloudfront resolveConfig branches and imported from the log_bucket_name Terraform output (back-compat: legacy configs → clear re-provision hint).
  • src/commands/insight.ts + src/index.ts: runInsight + insight <code> command (--days / --since).
  • infra/: v2 delivery pipeline + private log bucket + lifecycle + output + log_retention_days var.
  • README.md: usage + privacy notice (IPs stored only in the user's bucket, used only for unique approximation, minimal fields, 30-day expiry).

Testing

  • TDD throughout; new offline tests: test/cflog.test.ts, test/insight.test.ts, test/insight-command.test.ts, plus test/config.test.ts/test/init.test.ts cases.
  • npm run build + npm run typecheck clean. npx vitest run342 passed / 3 skipped / 1 failed, where the single failure is the pre-existing environment-dependent test/skill.test.ts preflight case (fails only when local AWS creds exist; unchanged on main; passes in CI — this branch touches neither that test nor preflight.mjs).

Verification boundary

infra/ could not be terraform validated in CI (no Terraform); schemas verified against provider docs and the committed lock file (aws 6.51.0, which has the aws_cloudwatch_log_delivery* resources). A real terraform apply against an account is a manual pre/post-merge check — the one surface CI structurally can't cover.

Follow-ups (tracked, non-blocking)

Refs #43

🤖 Generated with Claude Code

jkas2016 added 13 commits July 11, 2026 18:49
Design for the first vertical slice of `hostdoc insight <code>`: cloudfront
mode end-to-end (v2 delivery-pipeline logging → dedicated log bucket with
30-day expiry → CLI-side gzip/W3C parse → per-code hits + approx unique
visitors). Records the v1-vs-v2 decision (v2 chosen: ACL-free bucket-policy
delivery fits hostdoc's hardened private OAC buckets) and the metric scope.

Refs #43
TDD, bite-sized 8-task plan for the cloudfront insight slice: pure W3C
parser (T1) + aggregation/log-key selection (T2), getObjectBytes (T3),
config.logBucket + init import (T4), runInsight command (T5), CLI
registration (T6), v2 logging Terraform (T7), README privacy/usage (T8).

Refs #43
@jkas2016

jkas2016 commented Aug 7, 2026

Copy link
Copy Markdown
Owner Author

Review — approve, no blockers

Verified locally with main merged in (i.e. on top of #51 + #52):

  • npm run build + npm run typecheck clean; npx vitest run352 passed / 39 files, no failures (the skill.test.ts preflight case noted in the PR body did not fire in my environment).
  • The verification gap this PR flagged is now closed. [Task] CI에서 번들되는 infra/ Terraform을 fmt/validate로 검증 (HCL 배포물 사각지대) #54 / ci: validate bundled infra/ Terraform with fmt + validate (#54) #55 landed a credential-free terraform-validate CI job, and this branch's infra/ passes both terraform fmt -check and terraform validate (aws provider 6.51.0, init -backend=false). Rebasing on current main will run that automatically. A real apply is still a manual check, as you noted.
  • Design cross-checked against AWS standard logging (v2) docs and it holds up on the two points most likely to bite:
    • Cross-region is officially supported — "you must specify the US East (N. Virginia) Region (us-east-1) even if you want to enable cross Region delivery to another destination". So provider = aws.us_east_1 on the delivery resources with the bucket in the user's own region is correct, not a latent apply failure.
    • The log bucket policy matches the documented example exactly: delivery.logs.amazonaws.com principal, s3:x-amz-acl = bucket-owner-full-control, aws:SourceAccount, and ArnLike aws:SourceArn ...delivery-source:*.
    • Note for the manual apply: because destination_resource_arn is the bucket ARN without a prefix, CloudFront prepends AWSLogs/{account-id}/CloudFront/ ahead of your suffix_path. Real keys will be AWSLogs/<acct>/CloudFront/{DistributionId}/{yyyy}/{MM}/{dd}/{HH}/.... That's fine here — the bucket policy is scoped to /*, and cloudFrontLogKeyDate's /(\d{4})/(\d{2})/(\d{2})/ still anchors on the date segment (a 12-digit account id can't match \d{4}/) — but it's worth eyeballing once against a live bucket.

Non-blocking follow-ups

  1. Access logging has no off switch. aws_s3_bucket.logs + the delivery pipeline are unconditional, so an existing user who re-runs provision after upgrading starts collecting client IPs and paying S3 storage with no opt-out — only log_retention_days is tunable. An enable_access_logs variable (gating the resources with count) would make that a choice rather than a side effect of upgrading. Given the PR already ships a privacy notice in the README, this feels like the natural companion.
  2. --since is unvalidated. runInsight checks --days is a positive integer but passes --since straight through to a string comparison in filterLogKeys, so --since garbage silently yields "no matching requests yet" instead of an error. A YYYY-MM-DD shape check would keep the two flags symmetric.
  3. listKeys(s3, logBucket, "") always lists the whole bucket. The partitioned layout is exactly what Prefix is for; even --days 1 currently pages the full ~30-day keyspace before filtering client-side.
  4. All parsed records accumulate in one array before aggregateInsight runs. Aggregating per file (hits counter + IP set) would make memory O(unique IPs) instead of O(total requests) on a busy distribution.
  5. skills/hostdoc/SKILL.md doesn't know about insight. The README documents the command, but the agent skill's command list still stops at publish/list/open/rm — so the feature isn't reachable through the skill surface.

Only (1) changes user-visible behavior on upgrade; the rest are polish. Merging as-is per the agreed order (#51#52#54 → this).

@jkas2016

jkas2016 commented Aug 7, 2026

Copy link
Copy Markdown
Owner Author

Follow-ups from the review above are now tracked:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant