Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,20 @@ not a privacy platform (D41): the node is readable by design (discovery /
search / auditability), there is no default e2e, and the client is a PWA,
not a native app.

**Telemetry (D56):** web10 tracks hard — GA4 + Hotjar on every user-facing
surface (marketing site, social app, authenticator) — because it competes
with Meta and TikTok on user experience, and their UX is the output of a
decade of aggressive telemetry. The recording is content-blind by
construction (Hotjar `maskAllText` + `blockAllImages` — text blurred,
images blocked), and GA4 events are content-free by convention. **Content
is never tracked** — posts, DMs, media are not in the recordings, not in
the events, not sold, not fed to any ad machine (GA4 advertising features
stay off; the only sponsors a fan sees are the creator's, D50/D55). The
trade is terms-level, not a consent popup: "it is the wrong platform for
you if you arent ok with that." Full model:
`knowledge/knowledge-base/web10-v3/telemetry.md`. Do not "tighten" or
"privacy-wash" tracking without re-reading D56.

## The stack

- `api/` — FastAPI. The node. All data + auth + billing + media. Entry:
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ paid for value delivered, not for permission granted.
| --- | --- |
| **You own your data** | One collection per user — the record of your own life, held by you. Export it, move it, erase it. Delete means delete. |
| **No shadow ban** | Every post reaches every follower, by construction (fan-out on write). The feed is chronological, because a feed should report — not editorialize. |
| **Built like the best, owned like yours** | web10 tracks hard — GA4 + Hotjar, content masked — to compete with Meta and TikTok on user experience. Your content is never tracked, sold, or fed to an ad machine; the trade is stated in the terms, not hidden (D56). |
| **Apps are just frontends** | An app earns access through a scoped, expiring, revocable token. It never owns what it touches. |
| **Federated identity** | Identity is `(username, provider)`, like email. No central registry to petition, no account that can be taken from you. |
| **Private, not permanent** | Unlike a blockchain, your data can be private, temporary, and deletable. You set who sees it via terms — and delete means delete. |
Expand Down
22 changes: 22 additions & 0 deletions api/app/endpoints/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,28 @@ def patch_config(token: Token, update: ConfigUpdate):
return {"status": "updated", "changed": list(changes.keys())}


@router.get("/telemetry", tags=["system"])
def telemetry_config():
"""The node's telemetry IDs, for the frontends to install at runtime (D56).

Public — no token. GA4 measurement IDs and Hotjar site IDs are public
identifiers (they are embedded in every page's HTML for the scripts to
load); there is no secret here. CORS is wildcard on this node (the
security boundary is the token, not the origin), so every surface —
marketing site, social app, authenticator — can read this pre-login from
any origin.

The Node Config UI (admin) is where these are set; this endpoint is how
the values reach the client at runtime, so an operator can change the
IDs live without a rebuild. Empty string = that instrument is off.
"""
cfg = config_svc.effective_config()
return {
"ga4_measurement_id": cfg.get("ga4_measurement_id") or "",
"hotjar_site_id": cfg.get("hotjar_site_id") or "",
}


# --- Health ---


Expand Down
7 changes: 7 additions & 0 deletions api/app/models/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ class NodeConfig(BaseModel):
# Dev pay
dev_pay_pct: int = 98

# Telemetry (D56) — public analytics IDs, admin-set in the Node Config
# UI. Served to every surface at runtime via GET /telemetry. Empty = off.
ga4_measurement_id: str = ""
hotjar_site_id: str = ""

# Branding
brand_text: str = "web10"
logo_dark: str = ""
Expand Down Expand Up @@ -161,6 +166,8 @@ class ConfigUpdate(BaseModel):
stripe_live_credit_sub_id: str | None = None
stripe_live_space_sub_id: str | None = None
dev_pay_pct: int | None = None
ga4_measurement_id: str | None = None
hotjar_site_id: str | None = None
brand_text: str | None = None
logo_dark: str | None = None
logo_light: str | None = None
Expand Down
5 changes: 5 additions & 0 deletions api/app/services/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ def effective_config() -> dict:
"stripe_test_key": s.STRIPE_TEST_KEY,
"stripe_live_key": s.STRIPE_LIVE_KEY,
"dev_pay_pct": _as_int(s.DEV_PAY_PCT),
# Telemetry (D56) — public analytics IDs, admin-set in the Node
# Config UI. No settings.py default (empty = tracking off); the
# saved node_config is the only source. Served via GET /telemetry.
"ga4_measurement_id": "",
"hotjar_site_id": "",
"brand_text": "web10",
"logo_dark": "",
"logo_light": "",
Expand Down
81 changes: 81 additions & 0 deletions api/tests/test_node_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,3 +270,84 @@ def test_non_admin_cannot_read_config(self, token):
resp = tc.post("/config", json={"token": token})
assert resp.status_code == 403
assert "s3_secret_key" not in (resp.json() or {})


class TestTelemetryEndpoint:
"""GET /telemetry — the node's telemetry IDs (D56), served to every
surface at runtime so the admin can change them live without a rebuild.
Public: GA4/Hotjar IDs are public identifiers, no token, no secret."""

def test_public_no_token(self, client):
with patch("app.v3.services.clickhouse.client") as mock_ch:
mock_ch.query.return_value = _config_result(None)
resp = client.get("/telemetry")
assert resp.status_code == 200
assert resp.json() == {"ga4_measurement_id": "", "hotjar_site_id": ""}

def test_returns_saved_ids(self, client):
saved = {
"admins": ["jacoby149"],
"ga4_measurement_id": "G-ABC123",
"hotjar_site_id": "123456",
}
with patch("app.v3.services.clickhouse.client") as mock_ch:
mock_ch.query.return_value = _config_result(saved)
resp = client.get("/telemetry")
assert resp.status_code == 200
assert resp.json() == {"ga4_measurement_id": "G-ABC123", "hotjar_site_id": "123456"}

def test_partial_save_only_returns_that_id(self, client):
with patch("app.v3.services.clickhouse.client") as mock_ch:
mock_ch.query.return_value = _config_result({"ga4_measurement_id": "G-ONLY"})
resp = client.get("/telemetry")
assert resp.json() == {"ga4_measurement_id": "G-ONLY", "hotjar_site_id": ""}


class TestTelemetryConfigFields:
"""The two telemetry fields flow through effective_config (the Node
Config UI read) and /config/update (the admin write)."""

def test_effective_config_defaults_empty(self, client):
from app.services import config as config_svc

with patch("app.v3.services.clickhouse.client") as mock_ch:
mock_ch.query.return_value = _config_result(None)
cfg = config_svc.effective_config()
assert cfg["ga4_measurement_id"] == ""
assert cfg["hotjar_site_id"] == ""

def test_saved_ids_surface_in_effective_config(self, client):
from app.services import config as config_svc

saved = {"ga4_measurement_id": "G-XYZ", "hotjar_site_id": "999"}
with patch("app.v3.services.clickhouse.client") as mock_ch:
mock_ch.query.return_value = _config_result(saved)
cfg = config_svc.effective_config()
assert cfg["ga4_measurement_id"] == "G-XYZ"
assert cfg["hotjar_site_id"] == "999"

def test_config_update_accepts_telemetry_fields(self, client, token):
"""An admin can set the IDs via /config/update; they persist and
surface on the next /telemetry read. The endpoint takes two body
models — token (nested) + update (the field changes)."""
with patch("app.v3.services.clickhouse.client") as mock_ch:
# /config/update reads current, merges, saves (one query read).
mock_ch.query.return_value = _config_result({"admins": ["testuser"]})
resp = client.post(
"/config/update",
json={
"token": {"token": token},
"update": {"ga4_measurement_id": "G-NEW", "hotjar_site_id": "42"},
},
)
assert resp.status_code == 200
assert resp.json()["status"] == "updated"
assert set(resp.json()["changed"]) == {"ga4_measurement_id", "hotjar_site_id"}
# the merged body was persisted with the new IDs — find the
# node_config insert and check its body column (index 1 of the row).
node_inserts = [c for c in mock_ch.insert.call_args_list if c[0] and c[0][0] == "node_config"]
assert node_inserts, "expected a node_config insert"
row = node_inserts[-1][0][1][0]
inserted = json.loads(row[1])
assert inserted["ga4_measurement_id"] == "G-NEW"
assert inserted["hotjar_site_id"] == "42"
8 changes: 8 additions & 0 deletions knowledge/changelogs/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
3.27.3 || 28.08.2026
feature(api) + feature(ui) + feature(web10-social) + feature(marketing-ui) + fix(ui) + docs(kb): the telemetry IDs are runtime-configurable from the Node Config UI — no rebuild to change them. Operator: "you need to make it addable from the auth ui right? to change those ids!" **The problem:** 3.27.1 baked the GA4/Hotjar IDs at build time (Vite env → Dockerfile ARG), so changing them required a redeploy. **The fix:** the IDs now live in `node_config` (ClickHouse, same table as every other node setting) and are resolved at runtime. (1) **API:** `NodeConfig` + `ConfigUpdate` gain `ga4_measurement_id` + `hotjar_site_id`; `effective_config()` defaults them to `""`; new **public** `GET /telemetry` endpoint (no token — GA4/Hotjar IDs are public identifiers embedded in every page's HTML, not secrets; CORS is wildcard on this node so every surface reads it pre-login from any origin) returning the two IDs from the effective config. (2) **Auth UI:** a Telemetry card in the Node Config panel (GA4 Measurement ID + Hotjar Site ID inputs, blank = off, "applies live on next page load"). (3) **All three surfaces:** `src/lib/analytics.ts` gains `resolveTelemetryIds()` (fetches `GET /telemetry`; the node is authoritative when reachable, the build-time env is the fallback for pure frontend dev where the node is unreachable) + `loadGa4(id)` / `loadHotjar(id)` (idempotent) + `installTelemetry()` (fire-and-forget orchestrator). Each `main.tsx` calls `installTelemetry()`; the authenticator (single screen, no router) fires its one pageview after GA4 is ready. **Bug fix (exposed by the feature):** the Node Config save was broken — `saveConfig` + `saveAdmins` sent a flat body `{token, ...fields}` but `POST /config/update` takes two body models (`token: Token` + `update: ConfigUpdate`) and therefore expects `{token:{token}, update:{...}}`, so every save (main Save + Admins add/remove) 422'd. A `configUpdate(fields)` helper now builds the correct nested shape; pinned by new UI tests. **Tests:** 5 new API tests (public /telemetry empty/saved/partial; effective_config defaults + saved overlay; /config/update accepts the fields and persists them) + new/extended unit suites per app (loadGa4/loadHotjar idempotency + masking, resolveTelemetryIds node-vs-env precedence + fallback, installTelemetry) + new `configTelemetry.test.tsx` (telemetry fields render; Save + Admins send the nested shape; diff-only save). 801 API + 205 social + 235 marketing-ui + 122 ui tests green, tsc clean, builds clean. KB: `telemetry.md` updated (runtime resolution, the /telemetry endpoint, the save fix) + D56 addendum.

3.27.2 || 28.08.2026
docs(strategy) + docs(kb) + docs(readme): the positioning realignment — the docs stop reading "anti-analytics" and say the D56 game out loud. Operator: "can you updated the decisions the knowledge base, everything? because it as very anti analytics, but now that we arent doing the encryption privacy angle, we are trying to be influencer friendly, give them a better deal than these garbo social platforms, it changes the game!" **The reframe:** web10 is a data-policy platform that tracks hard — the incumbents' UX is the output of a decade of aggressive telemetry, and web10 now runs the same engine (GA4 + masked Hotjar, D56) with a data policy they can't offer (content never tracked, never sold, never fed to the ad machine; the only ads are the creator's, D50/D55). **The edits:** (1) `thesis.md` gains the "and it tracks hard (D56)" section — the data-policy frame is not a no-telemetry frame; the line (content is never tracked; advertising features off; the trade stated in the terms, not a consent popup); (2) `manifesto.md`'s "nobody is mining you" block is narrowed to what it was always about — the creator's content and the fan's words/pictures are never scanned, sold, or fed to an ad machine — and gains the candid parenthetical (we watch how people use the place, that's how it stays this good; your words and pictures are never part of it); (3) `AGENTS.md` gains the Telemetry (D56) paragraph in "What web10 is" — the operating rule for agents: do not "tighten" or "privacy-wash" tracking without re-reading D56; (4) `README.md`'s premise table gains the "Built like the best, owned like yours" row; (5) `design.md`'s self-hosted-fonts rule drops the stale "privacy-first product" justification (fonts are a core rendering dependency — reliability + control; telemetry scripts are a different category: intentional, disclosed, env-gated) and the FontAwesome retirement drops the "privacy leak" framing (render-blocking + uncontrolled third-party dependency). No code change.

3.27.1 || 28.08.2026
feature(marketing-ui) + feature(web10-social) + feature(ui) + docs(kb) + docs(strategy): D56 — full-platform telemetry: GA4 + Hotjar on every surface, content masked. Operator: "ultimate privacy isnt what web10 is about, we arent encrypting like whatsapp, we are doing analytics for influencers tracking, we should just put on max tracking like hotjar the shit out of the whole platform, we can blur photos and text, but so we can improve the platform, nothing wrong about hotjaring the shit out of this, it is the wrong platform for you if you arent ok with that" — then: "hotjar and google analytics" — then: "we should talk about the telemetry in the kb in a good way, we do alot of telemetry to compete with meta tik tok, to have the most competitive user experience." **The decision (D56):** every user-facing surface is tracked — GA4 + Hotjar on marketing-ui, web10-social, AND the authenticator (`ui/`). Supersedes the old "platform surfaces stay recording-free" rule (the comment that lived in the analytics modules). The recording is **content-blind by construction**: Hotjar initialises with `maskAllText: true` + `blockAllImages: true` (all text blurred, all images blocked — the operator sees cursor + layout + timing, never words or pictures); GA4 events stay content-free by convention (paths, actions, counts — never post text, media URLs, or PII). **Max tracking, one exception:** no privacy flags on GA4 (no IP anonymization — the operator asked for max, the trade is stated in the terms); the single kept flag is `advertising_id: 'OFF'` — we do not feed Google's ad network (the only sponsors a fan sees are the creator's, D50/D55). The trade is terms-level, not a consent popup. **The build:** (1) web10-social gains masked Hotjar (`installHotjar` in `src/lib/analytics.ts`, canonical `static.hotjar.com/c/hotjar-<id>.js?sv=6` snippet + `hj('init', {hjid, maskAllText, blockAllImages})`); its GA4 moves to the max-tracking config (drops `anonymize_ip`, keeps `advertising_id: 'OFF'`); `hotjarIdentify(username)` fires on login. (2) marketing-ui gains GA4 (`installGa4`, same pattern); its Hotjar moves from the old `script.hotjar.com` + `hj('initialize', id, version)` to the canonical masked init (the old `VITE_HOTJAR_VERSION` knob is retired). (3) The authenticator gains both — new `ui/src/lib/analytics.ts` (GA4 + masked Hotjar + `trackPageview`), installed in `main.tsx` with one pageview per load (it's query-parameter-driven, no router). All three are env-gated no-ops in dev (`VITE_GA4_MEASUREMENT_ID` / `VITE_HOTJAR_SITE_ID` unset → nothing loads). **Deploy wiring:** Dockerfile ARG/ENV on all three frontends; compose passes `GA4_MEASUREMENT_ID` / `HOTJAR_SITE_ID` per environment (empty = tracking off); env examples updated (`ubuntu-deployment/.env.example`, `ui/.env_example`). **KB:** new `knowledge-base/web10-v3/telemetry.md` (the Why layer — compete with Meta/TikTok on UX; the use case; the technical how; the line it does not cross: content never tracked, not sold, trade stated; logistics) + README links. **Tests:** new/extended unit suites per app — web10-social `analytics.test.ts` (Hotjar no-op / load / masking config pinned / idempotency / identify), marketing-ui `analytics.test.ts` (GA4 + masked Hotjar + the in-house beacon), ui `analytics.test.ts` (GA4 + masked Hotjar + pageview + identify). Screenshots not applicable (no visual change — tracking is invisible by design).
3.26.3 || 28.08.2026
test(e2e) + fix(social): the profiles surface e2e spec (social-e2e lane) + `profile.ts` addressed through the shared node-scoped `followersGroupId`. **Context:** 3.25.1 already fixed the shared `followersGroupId` helper (`src/data/groups.ts`) to be node-scoped (`{provider}/groups/users/{username}/followers`) — but `src/data/profile.ts` still carried its own local copy hardcoded to `web10.app/groups/{username}/followers`, so the profile read/write still addressed a group that never exists on a non-production node (every profile read 403'd, edits never persisted). **The fix:** `profile.ts` drops its local `followersGroupId` and imports the shared helper from `./groups`; `readProfile`/`saveProfile`/`readUserProfile` now use it. **The spec (`e2e/tests/social-profile.spec.ts`):** the API floor pins the app's exact read pattern (what `profile.ts` + `posts.ts` + `follows.ts` send) — the profile doc read, the posts read, the follower count — plus the I3 anti-test (a stranger's non-public data is not readable: a non-member's profile/posts read 403s, the members/list 401s) and the positive case (a follower CAN read the profile + posts). The browser gauntlet drives the real app pre-authed via the token cookie: own `/profile` (edit the bio → persists across a reload) → another user's public `/u/:username` (posts + follow UI) → the `/u/:username/p/:postId` deep link lands on the post. The followers group is created through the public API (`name: "followers"`) so the derived ID matches what the app computes. **Tests:** 197 social-app unit tests green, `tsc -b` + `vite build` clean, the 3 API-floor e2e tests green. **Note:** a fully green local browser-gauntlet run is blocked by the shared multi-workspace e2e stack (concurrent workspaces collide on the proxy/minio ports + the ClickHouse, tearing the stack down mid-run); the spec adds a signup/login retry + a short `settle()` after setup to ride out the ClickHouse merge race (no-ops on a healthy CI stack). The dedicated CI e2e stack does not have that conflict.

Expand Down
Loading
Loading