Contract for PostHog on danieldcs.com: event names, properties, privacy limits, and CTA mapping.
- PostHog US Cloud. Discard client IP data on the project. IP is used at ingest for GeoIP and bots, not stored.
- No PostHog cookieless mode (it drops IP before transforms and breaks GeoIP/bots).
- Client:
persistence: 'memory'— no cookies,localStorage, or consent banner. Uniques are inflated (newdistinct_idper full load); pageviews and custom events stay accurate. - Off: autocapture, session replay, surveys, feature flags.
- Init only when
NEXT_PUBLIC_VERCEL_ENV === 'production'and the public key exists; otherwise no-op. Skip init whennavigator.webdriver === true. posthog-jsis pinned (exact version) for the same reason as the slim entry and registered extensions—upgrades are an explicit task.- Client bootstrap: root
instrumentation-client.tsgates first, registers delegated click tracking (registerClickTrackingin capture phase), then lazy-loadslib/analytics-init.tsviascheduleOnLoad(load+requestIdleCallback, including whenreadyStateis alreadycomplete; slim SDK +historyAutocapture+webVitalsAutocapture+exceptionObserver/exceptions, with bundledposthog-js/dist/web-vitalsandposthog-js/dist/exception-autocapture). track()fromlib/analytics.ts: no-op when the gate is closed (events are not queued). When the gate is open and PostHog is not loaded yet, events are queued and flushed in order onloaded. If init fails, the queue is discarded silently.captureError()fromlib/analytics.ts: separate fromtrack()/AnalyticsEvent. No-op when the gate is closed (no queue). When the gate is open and PostHog is not loaded yet, errors are queued (max 10, oldest dropped) and flushed onloadedviabindErrorCapture. If init fails, the error queue is discarded with the event queue.
Not collected: stored IP, page text, input values, cross-visit identity.
Custom event properties: identifiers only (route id, slug, language, hostname)—not full URLs or query strings. PostHog's default properties ($current_url, $referrer, browser, OS, device, screen) are kept as sent, including query strings; the privacy policy discloses this.
Names are stable snake_case for track(). One click → one event (precedence).
| Event | Properties |
|---|---|
$pageview |
Super property language (pt | en) from the route, not Accept-Language |
$web_vitals |
Automatic, PostHog web vitals extension — inherits super property language |
$exception |
PostHog Error Tracking — unhandled errors and unhandled promise rejections (autocapture); React route/global boundaries via captureError() → captureException with boundary (route | global) and optional digest (Next.js). Message and stack come from PostHog; no capture_console_errors. |
article_read |
slug, language — fires once when the end of the post body enters the viewport (see below) |
external_link_click |
href_host, source — outbound link without data-cta |
cta_click |
cta, location, optional target |
cta_click fields: cta = kind below; location = header | footer | home | blog | about; target = nav segment, slug, or pt | en (language switched to).
- Fires when the sentinel after the MDX body intersects the viewport, once per post view.
- Short posts whose end is already visible on first paint fire on load; the signal means “reached the end,” not reading time.
- A full page reload sends a new event.
languageis the interface language for the route (same as$pageviewsuper property), notfrontmatter.language.
- Autocapture:
window.onerrorand unhandled promise rejections when production analytics is enabled. Console errors are off. The autocapture script is bundled (posthog-js/dist/exception-autocapture); the browser should not fetch PostHog CDN chunks for it. - Boundaries:
app/(pt)/error.tsx,app/(en)/en/error.tsx, andapp/global-error.tsxcallcaptureError()once per boundary render (React render errors are not double-counted with autocapture in normal cases). - Dashboard: PostHog Error tracking for
$exceptiongrouping and trends. - Limits: Client stacks are minified; no source maps in V1. SSG/build failures and server-only errors appear in Vercel logs, not PostHog.
- PostHog’s web vitals extension sends
$web_vitalswith LCP, CLS, FCP, and INP (not TTFB). Attribution is off (web_vitals_attribution: false); theweb-vitalsscript is bundled viaposthog-js/dist/web-vitals(no CDN fetch). - In PostHog: Web analytics → Web vitals. Good thresholds: LCP ≤ 2.5 s, INP ≤ 200 ms, CLS ≤ 0.1, FCP ≤ 1.8 s.
- Metrics reflect the initial load of each page, not client-side navigations. CLS and INP are flushed when the page is hidden or closed, so abrupt exits may drop samples.
Mobile header nav uses the same values as desktop.
| Where | UI | Event | cta / other |
|---|---|---|---|
| Header | Blog, Community, About | cta_click |
nav_item, target: blog | community | about |
| Footer | PT posts blog/livros-recomendados, blog/ferramentas-apps-e-setup; Privacy |
cta_click |
nav_item, target: path id or privacy |
| Footer | LinkedIn, GitHub, YouTube, Instagram | external_link_click |
source: footer, href_host |
| Home | About link; post cards | cta_click |
home_about; or post_card + target slug |
| Blog | Post cards (list/grid) | cta_click |
post_card, target: slug |
| About | LinkedIn trajectory; mailto contact |
cta_click |
linkedin_experience; contact_email |
| Header | Language switch | cta_click |
language_switch, target: pt | en |
data-ctaon element or ancestor →cta_clickonly.- Else external link →
external_link_clickonly.
Mark interactive elements in the UI; the delegated listener in lib/analytics-click.ts reads them at click time.
data-cta,data-cta-location, optionaldata-cta-target— map tocta_click(cta,location,target). Invaliddata-cta/data-cta-locationvalues are ignored (no event).data-analytics-sourceon an ancestor —LinkSourceforexternal_link_click(post,footer,about,community). Missing or invalid →unknown.- Middle-click (
auxclick) does not fireclick; no custom event is sent. - Listener uses capture phase, is registered only when the analytics gate is open, and runs before PostHog lazy-load; early clicks still queue via
track().
article_view(redundant with$pageviewon/blog/*)- Trilha,
/alunos(noindex/ interim)
Document name and properties here first. Stable snake_case; identifiers only; one click, one event.
Configuration outside this repo that the code and the privacy policy depend on. Operational details (channels, accounts) stay out of this repository.
PostHog — project Daniel's Blog (US Cloud)
- Discard client IP data: on (the privacy policy depends on it)
- Cookieless tracking: off (it would disable GeoIP and bot detection)
- Transformations: GeoIP and Bot detection on
- Session replay and surveys: off
- A separate test project (
<nome>) is used for local validation; never put its key in Vercel.
Vercel
NEXT_PUBLIC_POSTHOG_KEYis set for Production only; preview and development send nothing.- Changing the key requires a production redeploy (
NEXT_PUBLIC_*is inlined at build time). - Failed deployment notifications are enabled.
Uptime — Better Stack
- Monitors
https://danieldcs.com; alerts go to Slack.