chore/ph-annot
needs attentionviewing older commit3a88710 · fullpre-PRreviewed 2026-07-30 01:43 UTC0H · 2M · 7L · 7I- Purpose
- Add PostHog deploy annotation markers to the prod release pipeline so metric changes in dashboards can be attributed to specific releases at a glance.
- Goal
- Annotate every PostHog time-series with a vertical deploy marker after each successful Vercel production release, turning the analytics timeline into a causal narrative.
- Sub-goals
- SG-1: New posthog-annotate CI job in release-promote.yml (NON-GATING, continue-on-error)
- SG-2: Standalone scripts/posthog-annotate.mjs that POSTs a deploy annotation to the PostHog API
- What
- Added a new posthog-annotate GitHub Actions job that runs after preflight + vercel-promote + postflight, and a new scripts/posthog-annotate.mjs script that calls the PostHog annotations API.
- Why
- Without deploy markers, a metric jump is indistinguishable from a behaviour change, a product change, or an instrumentation regression.
- Areas
- .github/workflows/release-promote.yml+42−0scripts/posthog-annotate.mjs+99−0
- Blast
- 2 files, +141 lines. CI-only change — no app code, domain logic, or database touched. Non-gating (continue-on-error: true).
Findings · 15
correctness3
Script exits 0 on auth failure, masking misconfigured secrets
scripts/posthog-annotate.mjs:40
When the API returns 401/403 (wrong or expired key), the script logs a warning and exits 0. The job is continue-on-error:true so a non-zero exit would still not block the release — but it would surface as a failed step in the UI, making the misconfiguration visible. Consider exiting 1 on 4xx auth errors so operators notice the credential problem.
POSTHOG_PROJECT_ID hardcoded as string literal
.github/workflows/release-promote.yml
Not a security issue (project IDs are not secret), but a project migration requires a code change rather than a config update.
date_marker uses time of annotation job, not actual deploy timestamp
scripts/posthog-annotate.mjs:27
If the posthog-annotate job is delayed by queue time, the annotation timestamp will be slightly later than the real moment. Acceptable, but passing a preflight output timestamp would be more precise.
security4
SSRF via user-controlled POSTHOG_HOST variable
.github/workflows/release-promote.yml
POSTHOG_HOST is sourced from a GitHub Actions variable (vars.POSTHOG_HOST) with a fallback to 'https://us.posthog.com'. A compromised or misconfigured variable could redirect the HTTP POST — carrying the POSTHOG_PERSONAL_API_KEY bearer token — to an attacker-controlled host. The script only strips a trailing slash with no origin allowlist or URL validation. Mitigation: hardcode the host constant in the script, or assert the scheme is https and the hostname ends with `.posthog.com` before fetching.
Actor and deployment URL embedded in annotation content without sanitisation
scripts/posthog-annotate.mjs
github.actor and the production deployment URL are interpolated directly into the annotation content. Low impact here (internal PostHog annotation), but a actor.replace guard adds defence-in-depth against control-character injection in CI logs.
API key could surface via error response echoing
scripts/posthog-annotate.mjs
On non-OK response, up to 300 chars of raw response body are logged. If PostHog ever echoes back auth context in error bodies, the key could surface in CI logs. Consider omitting the body on 401/403.
actions/checkout and actions/setup-node not pinned to commit SHAs
.github/workflows/release-promote.yml
Uses floating @v4 tags. A compromised upstream action repo could execute arbitrary code in a job that holds POSTHOG_PERSONAL_API_KEY. Note: if the rest of the workflow uses the same floating tags this is consistent rather than a regression.
conventions1
Emoji in annotation content may cause encoding issues in some log aggregators
scripts/posthog-annotate.mjs
buildContent() emits a rocket emoji. PostHog's API handles UTF-8, but some CI log consumers or annotation search UIs can mishandle non-ASCII. Confirm PostHog renders it correctly or consider an ASCII alternative.
tests2
No unit tests for buildContent() or main() logic
scripts/posthog-annotate.mjs
The script contains non-trivial logic: buildContent() assembles content from multiple env vars with fallbacks and conditional concatenation; main() guards on missing credentials, handles HTTP errors, and parses JSON. These are testable functions. A typo in buildContent or wrong HOST interpolation would silently produce a malformed annotation. The continue-on-error:true boundary limits blast radius but does not substitute for test coverage.
buildContent is not exported — testability requires minor refactor
scripts/posthog-annotate.mjs:8
The top-level main().catch(...) call makes importing the file for unit tests awkward. Exporting buildContent as a named export would make the content logic trivially testable.
improvement5
POSTHOG_PROJECT_ID hardcoded in workflow instead of a variable
.github/workflows/release-promote.yml
POSTHOG_PROJECT_ID is hardcoded as "334265" inline. POSTHOG_HOST correctly uses vars.POSTHOG_HOST — this should use vars.POSTHOG_PROJECT_ID for consistency, and to avoid a code change if the project ID ever changes.
Checkout + Node setup just to run a single fetch call adds ~20-30s
.github/workflows/release-promote.yml
The job checks out the full repo and installs Node 20 to run a ~30-line script that does one HTTP POST with no npm deps. A curl one-liner would be faster and remove the Node version dependency entirely. Not blocking, but worth considering for job startup time.
buildContent() conditional appends could use filter/join
scripts/posthog-annotate.mjs
The four conditional appends could be: [env, sha, actor && 'by ' + actor, url].filter(Boolean).join(' · ') — more idiomatic and easier to extend.
res.text() truncation limit (300 chars) is undocumented
scripts/posthog-annotate.mjs
Truncating error body to 300 chars is reasonable but the limit is arbitrary. A comment explaining the rationale would help future editors.
date_marker anchored to annotation time rather than commit time
scripts/posthog-annotate.mjs
Passing the Git commit author date via a preflight output would anchor the marker to when the code shipped rather than when the CI step ran. Low impact since the job runs immediately after promote.