Skip to content

feat(telemetry): local product-event capture — activation funnel, Tier-1 (ent#184) - #1721

Merged
vybe merged 1 commit into
devfrom
feature/ent184-product-events-capture
Jul 22, 2026
Merged

feat(telemetry): local product-event capture — activation funnel, Tier-1 (ent#184)#1721
vybe merged 1 commit into
devfrom
feature/ent184-product-events-capture

Conversation

@dolho

@dolho dolho commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

ent#184 — Tier-1 local product events (OSS capture + gated view)

Gating shape (confirmed): OSS-core capture + entitlement-gated funnel view. This is the public-repo half; the enterprise funnel backend ships in trinity-enterprise (feature/184-telemetry-funnel).

Capture — OSS-core, default-ON, zero network egress

  • product_events table (installation_id, event_type, event_context, created_at) — db/schema.py + db/tables.py MetaData + dual-track migration (SQLite product_events_table + Alembic 0029_product_events).
  • POST /api/product-events — writes ONE local row and returns; never phones home. event_type checked against a fixed allow-list (unknown → 422) so the table can't be spammed; context byte-capped.
  • OnboardingWizard.vue emits its step transitions (setup_startedsetup_step_createsetup_step_credentialsetup_completed/setup_dismissed) via stores/productTelemetry.js — fire-and-forget, never blocks/breaks the wizard.
  • First-value events (first_agent_created, first_chat, …) are derived on read by the enterprise view from audit_log/agent_activities/agent_schedules — never re-emitted, so they survive restart by construction.
  • db/product_events.pyProductEventOperations (record + count-by-type + chronological list for Tier-2 backfill + optional prune), wired into the facade.

Funnel view — ships in the OSS bundle, hidden unless entitled

  • components/settings/ActivationFunnelPanel.vue — step funnel + drop-off % + first-value tiles + honest empty state; fetches the gated enterprise endpoint.
  • Settings.vue gains an admin-only Activation tab, requires: 'telemetry' → invisible in OSS-only builds (standard feature-flag gating).

Docs

  • Requirements §45 (lifecycle-observability.md) — generic seam only (no enterprise module design in public docs).
  • User-facing docs/PRODUCT_EVENTS.md — what's recorded, that it never leaves the box, how to view.

AC coverage

  • Wizard step transitions + first-value events recorded/derived locally, default-ON, survive restart
  • Operator-facing funnel with step counts / drop-off + honest empty state (view lands with the enterprise PR)
  • Zero network egress — the emit endpoint writes one local row; documented local-only
  • Events queryable with timestamps + installation_id for fix: Add ARM64 Alpine compatibility for frontend Docker build #12's opt-in backfill
  • Documented in user docs

Verification

Verified end-to-end on the local Postgres stack: capture POST + allow-list 422 + (with the enterprise submodule mounted) the funnel returning captured steps and derived first-value from real data. tests/unit/test_ent184_product_events_db.py — 6 passing.

Related to ent#184 · pairs with trinity-enterprise feature/184-telemetry-funnel

🤖 Generated with Claude Code

…r-1 (ent#184)

OSS-core CAPTURE half of the two-tier telemetry model: records activation events
on the operator's own instance, default-ON, with ZERO network egress. The
operator-facing funnel VIEW is an entitlement-gated enterprise surface (gating
shape confirmed: OSS capture + gated view) — the enterprise `telemetry` module
ships separately in the private submodule and reads these local rows.

Capture (this PR, every edition):
- product_events table (installation_id, event_type, event_context, created_at):
  db/schema.py + db/tables.py MetaData + dual-track migration (SQLite
  db/migrations.py `product_events_table` + Alembic 0029_product_events).
- POST /api/product-events (routers/product_events.py) — writes one local row
  and returns; never phones home. event_type is checked against a fixed
  allow-list (unknown → 422) so the table can't be spammed; context byte-capped.
- OnboardingWizard.vue emits its step transitions (setup_started →
  setup_step_create → setup_step_credential → setup_completed / setup_dismissed)
  through stores/productTelemetry.js — fire-and-forget, never blocks/breaks the
  wizard. First-value events (first_agent_created, first_chat, ...) are DERIVED
  on read by the enterprise view from audit_log/agent_activities, never emitted.
- db/product_events.py: ProductEventOperations (record + count-by-type +
  chronological list for Tier-2 backfill + optional prune), wired into the facade.

Gated funnel view (ships in the OSS bundle, hidden unless entitled):
- components/settings/ActivationFunnelPanel.vue — step funnel + drop-off +
  first-value tiles + honest empty state; fetches the gated enterprise endpoint.
- Settings.vue gains an admin-only `Activation` tab, `requires: 'telemetry'`, so
  it's invisible in OSS-only builds (standard feature-flag gating).

Docs: requirements §45 (lifecycle-observability.md) + user-facing
docs/PRODUCT_EVENTS.md (what's recorded, that it never leaves the box, how to
view). Test: tests/unit/test_ent184_product_events_db.py (record/count/list/
since-window/prune round-trips).

Verified end-to-end on the local Postgres stack: capture POST + allow-list 422 +
the enterprise funnel returning the captured steps and derived first-value.

Related to ent#184
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@vybe vybe left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validated via /validate-pr: required checks green, security scan clean, dual-track migration present (SQLite + Alembic 0029).

@vybe
vybe merged commit 7ee9a19 into dev Jul 22, 2026
25 checks passed
vybe pushed a commit that referenced this pull request Jul 22, 2026
…kfill (ent#12)

The opt-IN egress layer on top of Tier-1 (ent#184). Default-OFF, reversible.
Gating shape confirmed: OSS-core consent+egress+backfill, enterprise-gated
benchmark carrot (the reward). Stacked on the ent#184 OSS branch.

Never egresses without consent — TWO independent gates, both re-checked in
share_now: the stored `telemetry_sharing_enabled` consent (default-off) AND the
config switch `TELEMETRY_SHARING_ENABLED` (honors DO_NOT_TRACK). Either off ⇒
nothing leaves the box.

- services/telemetry_sharing_service.py: consent state (system_settings),
  build_aggregate_payload (anonymized — installation_id + version/edition/
  platform/python + coarse enterprise_features + agent/execution COUNTS + Tier-1
  activation-funnel counts; NO PII, content, prompts, emails, or agent names),
  share_now (gated, fail-open, honest 2xx), and a sleeps-first jittered heartbeat
  (default 24h) that shares when consent is on. Reuses the operator-intake #38
  credential-free httpx transport.
- Consent-time retroactive backfill: on the off→on transition the router
  schedules an immediate fire-and-forget backfill over a disclosed window
  (default 30d) sourced from Tier-1 product_events.
- API (routers/settings.py): GET /api/settings/telemetry-sharing (status +
  inspectable payload_preview — the operator sees EXACTLY what would be sent
  before consenting) + PUT (admin + human-only, audit-logged, 409 when hard-
  disabled). feature-flags exposes telemetry_sharing_enabled (observability).
- config.py: TELEMETRY_SHARING_ENABLED/URL/INTERVAL_HOURS/BACKFILL_DEFAULT_DAYS.
- lifespan: staggered heartbeat start (+9s).
- Frontend: value-framed, optional, non-blocking consent ask in the onboarding
  wizard (hidden when hard-disabled) + a reversible default-off Settings → General
  panel (TelemetrySharingPanel.vue) with the inspectable payload preview + backfill
  picker; the gated ActivationFunnelPanel shows the benchmark reciprocity status.
- Docs: docs/PRODUCT_EVENTS.md Tier-2 section (payload schema + reversibility +
  backfill) + requirements §45.1.

Tests: tests/unit/test_ent12_telemetry_sharing.py (8) — no-PII payload, both-gate
egress, opt-out stops egress, fail-open, non-2xx not marked shared, consent
round-trip. Verified live on the Postgres stack: gating + delivery to a local
sink + no-PII + reversibility (21 checks), and the router/preview end-to-end. No
real external egress in any test.

Related to ent#184 (Tier-1) · pairs with trinity-enterprise benchmark carrot.
Depends on #1721 (ent#184 OSS) — rebase onto dev when it merges.

Related to ent#12
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
vybe pushed a commit that referenced this pull request Jul 23, 2026
…kfill (ent#12) (#1723)

* feat(telemetry): Tier-2 opt-in fleet sharing — consent + egress + backfill (ent#12)

The opt-IN egress layer on top of Tier-1 (ent#184). Default-OFF, reversible.
Gating shape confirmed: OSS-core consent+egress+backfill, enterprise-gated
benchmark carrot (the reward). Stacked on the ent#184 OSS branch.

Never egresses without consent — TWO independent gates, both re-checked in
share_now: the stored `telemetry_sharing_enabled` consent (default-off) AND the
config switch `TELEMETRY_SHARING_ENABLED` (honors DO_NOT_TRACK). Either off ⇒
nothing leaves the box.

- services/telemetry_sharing_service.py: consent state (system_settings),
  build_aggregate_payload (anonymized — installation_id + version/edition/
  platform/python + coarse enterprise_features + agent/execution COUNTS + Tier-1
  activation-funnel counts; NO PII, content, prompts, emails, or agent names),
  share_now (gated, fail-open, honest 2xx), and a sleeps-first jittered heartbeat
  (default 24h) that shares when consent is on. Reuses the operator-intake #38
  credential-free httpx transport.
- Consent-time retroactive backfill: on the off→on transition the router
  schedules an immediate fire-and-forget backfill over a disclosed window
  (default 30d) sourced from Tier-1 product_events.
- API (routers/settings.py): GET /api/settings/telemetry-sharing (status +
  inspectable payload_preview — the operator sees EXACTLY what would be sent
  before consenting) + PUT (admin + human-only, audit-logged, 409 when hard-
  disabled). feature-flags exposes telemetry_sharing_enabled (observability).
- config.py: TELEMETRY_SHARING_ENABLED/URL/INTERVAL_HOURS/BACKFILL_DEFAULT_DAYS.
- lifespan: staggered heartbeat start (+9s).
- Frontend: value-framed, optional, non-blocking consent ask in the onboarding
  wizard (hidden when hard-disabled) + a reversible default-off Settings → General
  panel (TelemetrySharingPanel.vue) with the inspectable payload preview + backfill
  picker; the gated ActivationFunnelPanel shows the benchmark reciprocity status.
- Docs: docs/PRODUCT_EVENTS.md Tier-2 section (payload schema + reversibility +
  backfill) + requirements §45.1.

Tests: tests/unit/test_ent12_telemetry_sharing.py (8) — no-PII payload, both-gate
egress, opt-out stops egress, fail-open, non-2xx not marked shared, consent
round-trip. Verified live on the Postgres stack: gating + delivery to a local
sink + no-PII + reversibility (21 checks), and the router/preview end-to-end. No
real external egress in any test.

Related to ent#184 (Tier-1) · pairs with trinity-enterprise benchmark carrot.
Depends on #1721 (ent#184 OSS) — rebase onto dev when it merges.

Related to ent#12
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(telemetry): review fixes — real audit enum, compose wiring, catch-all block, wizard disclosure (ent#12)

Validation-pass fixes on the Tier-2 sharing PR:
- AuditEventType.SETTINGS does not exist — the consent audit call raised
  AttributeError into the best-effort except and never logged. Use
  CONFIGURATION like every sibling settings audit.
- Wire TELEMETRY_SHARING_{ENABLED,URL,INTERVAL_HOURS,BACKFILL_DEFAULT_DAYS}
  into both compose backend.environment blocks + .env.example — the documented
  hard kill switch was inert in every containerized deploy (#1056 class).
- Block the telemetry_sharing_* key family in the generic settings PUT — the
  dedicated route's human-only gate (reject_agent_principal + 409 + audit) was
  bypassable by an admin-owned agent-scoped key (trinity-ops-agent#232 class).
- Wizard consent copy now discloses the 30-day backfill (FR-4).

Two regression tests pin the enum and the blocklist.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Eugene Vyborov <eugene@beingluminous.com>
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.

2 participants